Skip to content

Commit fd0574d

Browse files
Cr 2066 (#14)
* Refactor Dockerfile * Bump version * Fix bolter args Co-authored-by: Pavel Nosovets <pavel.nosovets@codefresh.io>
1 parent 403cce6 commit fd0574d

File tree

10 files changed

+456
-7
lines changed

10 files changed

+456
-7
lines changed

Dockerfile

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
ARG DOCKER_VERSION=18.09.5
22

3-
FROM quay.io/prometheus/node-exporter:v0.15.1 AS node-exporter
4-
# install node-exporter
3+
# dind-cleaner
4+
FROM golang:1.9.2 AS cleaner
5+
RUN curl https://glide.sh/get | sh
56

6-
FROM codefresh/dind-cleaner:v1.1 AS dind-cleaner
7+
COPY cleaner/dind-cleaner/glide* /go/src/github.com/codefresh-io/dind-cleaner/
8+
WORKDIR /go/src/github.com/codefresh-io/dind-cleaner/
79

8-
FROM codefresh/bolter AS bolter
10+
RUN mkdir -p /go/src/github.com/codefresh-io/dind-cleaner/{cmd,pkg}
11+
RUN glide install --strip-vendor && rm -rf /root/.glide
912

13+
COPY cleaner/dind-cleaner/cmd ./cmd/
14+
15+
RUN CGO_ENABLED=0 go build -o /usr/local/bin/dind-cleaner ./cmd && \
16+
chmod +x /usr/local/bin/dind-cleaner && \
17+
rm -rf /go/*
18+
19+
# bolter
20+
FROM golang:1.12.6-alpine3.9 AS bolter
21+
RUN apk add git
22+
RUN go get -u github.com/hasit/bolter
23+
24+
# node-exporter
25+
FROM quay.io/prometheus/node-exporter:v1.0.0 AS node-exporter
26+
27+
# Main
1028
FROM docker:${DOCKER_VERSION}-dind
1129

1230
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.11/main' >> /etc/apk/repositories \
@@ -15,7 +33,7 @@ RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.11/main' >> /etc/apk/repositor
1533
&& rm -rf /var/cache/apk/*
1634

1735
COPY --from=node-exporter /bin/node_exporter /bin/
18-
COPY --from=dind-cleaner /usr/local/bin/dind-cleaner /bin/
36+
COPY --from=cleaner /usr/local/bin/dind-cleaner /bin/
1937
COPY --from=bolter /go/bin/bolter /bin/
2038

2139
WORKDIR /dind

cleaner/dind-cleaner/cmd/main.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
Copyright 2018 The Codefresh Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"time"
21+
"flag"
22+
"os"
23+
"bufio"
24+
"github.com/golang/glog"
25+
"github.com/docker/docker/api/types"
26+
"github.com/docker/docker/client"
27+
"golang.org/x/net/context"
28+
)
29+
30+
func readFileLines(path string) ([]string, error) {
31+
var lines []string
32+
if path == "" {
33+
return lines, nil
34+
}
35+
file, err := os.Open(path)
36+
if err != nil {
37+
return nil, err
38+
}
39+
defer file.Close()
40+
41+
scanner := bufio.NewScanner(file)
42+
for scanner.Scan() {
43+
lines = append(lines, scanner.Text())
44+
}
45+
return lines, scanner.Err()
46+
}
47+
48+
var dryRun *bool
49+
const (
50+
cmdImages = "images"
51+
52+
statusFound = "found"
53+
statusRemoved = "removed"
54+
statusRetainedByList = "retainedByList"
55+
statusRetainedByDate = "retainedByDate"
56+
statusChildRetained = "childRetained"
57+
statusChildFailedToRemove = "childFailedToRemove"
58+
statusFailedToRemove = "failedToRemove"
59+
)
60+
61+
func _stringInList(list []string, s string) bool {
62+
for _, a := range list {
63+
if a == s {
64+
return true
65+
}
66+
}
67+
return false
68+
}
69+
70+
func cleanImages(retainedImagesList []string, retainPeriod int64) {
71+
glog.Infof("Entering cleanImages, length of retainedImagesList = %d", len(retainedImagesList))
72+
if os.Getenv("DOCKER_API_VERSION") == "" {
73+
os.Setenv("DOCKER_API_VERSION", "1.35")
74+
}
75+
76+
cli, err := client.NewEnvClient()
77+
if err != nil {
78+
panic(err)
79+
}
80+
81+
type imageToCleanStruct = struct {
82+
ID string
83+
Created int64
84+
ParentID string
85+
status string
86+
tags []string
87+
childrenIDs map[string]string
88+
size int64
89+
}
90+
91+
92+
/*
93+
Purpose: remove images starting from first child excluding ids in retainedImagesList
94+
Logic:
95+
1. get All images (with All=true)
96+
2. fill map of imageToCleanStruct - for each image fill its children in the map of [id]"status"
97+
3. find images with no children
98+
4. loop by found images with no children and delete them, then update childrenList of whole map of imageToCleanStruct.
99+
Skip deletion for images in retainedImagesList
100+
--- Repeat 3-4 until images to delete found
101+
102+
*/
103+
104+
// 1. Get All Images
105+
ctx := context.Background()
106+
imagesFullList, err := cli.ImageList(ctx, types.ImageListOptions{All: true})
107+
if err != nil {
108+
panic(err)
109+
}
110+
111+
glog.Infof("Found %d images in docker", len(imagesFullList))
112+
113+
currentTs := time.Now().Unix()
114+
// 2. fill map of imageToCleanStruct
115+
images := make(map[string]*imageToCleanStruct)
116+
for _, img := range imagesFullList {
117+
images[img.ID] = &imageToCleanStruct{
118+
ID: img.ID,
119+
Created: img.Created,
120+
ParentID: img.ParentID,
121+
status: statusFound,
122+
tags: img.RepoTags,
123+
size: img.Size,
124+
childrenIDs: make(map[string]string),
125+
}
126+
}
127+
128+
glog.Infof("Calculating child images ...")
129+
for imageID, img := range images {
130+
if img.ParentID != "" {
131+
parentImage, parentImageInList := images[img.ParentID]
132+
if parentImageInList {
133+
parentImage.childrenIDs[imageID] = statusFound
134+
}
135+
}
136+
}
137+
138+
// Loop until found some imagesToDelete
139+
var imagesToDelete []string
140+
loopCount := 0
141+
for {
142+
imagesToDelete = nil
143+
loopCount++
144+
// 3. finding all images with no children
145+
glog.Infof("\n\n#################\n------ Loop %d - finding images without any children to remove ...", loopCount)
146+
for imageID, img := range images {
147+
if len(img.childrenIDs) == 0 && (img.status == statusFound || img.status == statusChildFailedToRemove) {
148+
imagesToDelete = append(imagesToDelete, imageID)
149+
}
150+
}
151+
152+
if len(imagesToDelete) == 0 {
153+
glog.Infof("Stopping - no images leave to remove ...")
154+
break
155+
}
156+
157+
// 4. Loop by found images and delete|retain , then update whole images map
158+
glog.Infof("Found %d images with no children", len(imagesToDelete))
159+
for _, imageID := range imagesToDelete {
160+
glog.Infof("\n Check if to remove image %s - %v", imageID, images[imageID].tags)
161+
// checking if image in retained list
162+
163+
if _stringInList(retainedImagesList, imageID) {
164+
glog.Infof(" Skiping image %s - %v , it appears in retained list", imageID, images[imageID].tags)
165+
images[imageID].status = statusRetainedByList
166+
} else if retainPeriod > 0 && images[imageID].Created > 0 && images[imageID].Created < currentTs &&
167+
currentTs - images[imageID].Created < retainPeriod {
168+
169+
glog.Infof(" Skiping image %s - %v , its created more than retainPeriod %d seconds ago", imageID, images[imageID].tags, retainPeriod)
170+
images[imageID].status = statusRetainedByDate
171+
} else {
172+
glog.Infof(" Deleting image %s - %v", imageID, images[imageID].tags)
173+
// add image delete here
174+
var err error
175+
if !*dryRun {
176+
_, err = cli.ImageRemove(ctx, imageID, types.ImageRemoveOptions{Force: true, PruneChildren: false})
177+
} else {
178+
glog.Infof( "DRY RUN - do not actually delete")
179+
}
180+
181+
if err == nil {
182+
glog.Infof(" image %s - %v has been deleted", imageID, images[imageID].tags)
183+
images[imageID].status = statusRemoved
184+
} else {
185+
glog.Infof(" FAILED to delete image %s - %v - %v", imageID, images[imageID].tags, err)
186+
images[imageID].status = statusFailedToRemove
187+
}
188+
}
189+
190+
glog.Infof(" setting image status to %s", images[imageID].status)
191+
for _, img := range images {
192+
if _, ok := img.childrenIDs[imageID]; ok {
193+
if images[imageID].status == statusRemoved {
194+
glog.Infof(" deleting the child from parent image %s - %v", img.ID, img.tags)
195+
delete(img.childrenIDs, imageID)
196+
} else if images[imageID].status == statusRetainedByList || images[imageID].status == statusRetainedByDate {
197+
glog.Infof(" setting child status %s for image %s - %v", images[imageID].status, img.ID, img.tags)
198+
img.childrenIDs[imageID] = images[imageID].status
199+
img.status = statusChildRetained
200+
201+
} else if images[imageID].status == statusFailedToRemove {
202+
glog.Infof(" setting child status %s and deleting the from parent image %s - %v", images[imageID].status, img.ID, img.tags)
203+
delete(img.childrenIDs, imageID)
204+
img.status = statusChildFailedToRemove
205+
}
206+
}
207+
}
208+
}
209+
}
210+
211+
glog.Info("\n################\nPrinting results ..")
212+
var totalImagesSize, removedSize, retainedByListSize, retainedByDateSize, failedToRemoveSize int64
213+
for _, img := range images {
214+
glog.Infof("%s: %v - %s, size = %d", img.status, img.tags, img.ID, img.size)
215+
for childID, childStatus := range img.childrenIDs {
216+
glog.Infof(" Child: %s - %s (grandchild retained)", childID, childStatus)
217+
}
218+
219+
totalImagesSize += img.size
220+
switch img.status {
221+
case statusRemoved:
222+
removedSize += img.size
223+
case statusRetainedByList:
224+
retainedByListSize += img.size
225+
case statusRetainedByDate:
226+
retainedByDateSize += img.size
227+
case statusFailedToRemove:
228+
failedToRemoveSize += img.size
229+
}
230+
}
231+
232+
glog.Infof("\n-----------\n" +
233+
" total images shared size: %.3f Mb \n" +
234+
" removed shared size: %.3f Mb \n" +
235+
"retained shared by list size: %.3f Mb \n" +
236+
"retained shared by date size: %.3f Mb \n" +
237+
" failed to remove size: %.3f Mb ",
238+
float64(totalImagesSize)/1024/1024.0,
239+
float64(removedSize)/1024/1024.0,
240+
float64(retainedByListSize)/1024/1024.0,
241+
float64(retainedByDateSize)/1024/1024.0,
242+
float64(failedToRemoveSize)/1024/1024.0)
243+
}
244+
245+
func main() {
246+
247+
usage := `
248+
Usage: dind-cleaner <command> [options]
249+
250+
Commands:
251+
images [--retained-images-file] [--dry-run]
252+
`
253+
flag.Parse()
254+
flag.Set("v", "4")
255+
flag.Set("alsologtostderr", "true")
256+
validCommands := []string{"images"}
257+
if len(os.Args) < 2 {
258+
glog.Errorf("%s", usage)
259+
os.Exit(2)
260+
} else if !_stringInList(validCommands,os.Args[1]) {
261+
glog.Errorf("Invalid command %s\n%s", os.Args[1], usage)
262+
os.Exit(2)
263+
}
264+
265+
imagesCommand := flag.NewFlagSet("images", flag.ExitOnError)
266+
retainedImagesListFile := imagesCommand.String("retained-images-file", "", "Retained images list file")
267+
imageRetainPeriod := imagesCommand.Int64("image-retain-period", 86400, "image retain period")
268+
269+
dryRun = imagesCommand.Bool("dry-run", false, "dry run - only print actions")
270+
271+
switch os.Args[1] {
272+
case "images":
273+
imagesCommand.Parse(os.Args[2:])
274+
imagesCommand.Set("v", "4")
275+
default:
276+
glog.Errorf("%q is not valid command.\n", os.Args[1])
277+
os.Exit(2)
278+
}
279+
280+
if os.Getenv("CLEANER_DRY_RUN") != "" {
281+
*dryRun = true
282+
}
283+
284+
285+
glog.Infof("\n----------------\n Started dind-cleaner")
286+
287+
glog.Infof("First verson - only image cleaner. " +
288+
"retainedImagesListFile = %s " +
289+
"retainedImagesPeriod = %d " +
290+
"dry-run = %t" , *retainedImagesListFile, *imageRetainPeriod, *dryRun)
291+
292+
retainedImagesList, err := readFileLines(*retainedImagesListFile)
293+
if err != nil {
294+
glog.Errorf("Failed to read file %s: %v", *retainedImagesListFile, err)
295+
}
296+
cleanImages(retainedImagesList, *imageRetainPeriod)
297+
}

0 commit comments

Comments
 (0)