Skip to content

Commit 334f8a0

Browse files
committed
Add GC for docker images
1 parent f614d82 commit 334f8a0

File tree

6 files changed

+620
-1
lines changed

6 files changed

+620
-1
lines changed

internal/common/bytesize.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package common
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"math"
11+
"regexp"
12+
"strconv"
13+
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
// Common units for sizes in bytes.
18+
const (
19+
Byte = ByteSize(1)
20+
KiloByte = 1024 * Byte
21+
MegaByte = 1024 * KiloByte
22+
GigaByte = 1024 * MegaByte
23+
)
24+
25+
const (
26+
byteString = "B"
27+
kiloByteString = "KB"
28+
megaByteString = "MB"
29+
gigaByteString = "GB"
30+
)
31+
32+
// ByteSize represents the size of a file.
33+
type ByteSize uint64
34+
35+
// Ensure FileSize implements these interfaces.
36+
var (
37+
_ json.Marshaler = new(ByteSize)
38+
_ json.Unmarshaler = new(ByteSize)
39+
_ yaml.Marshaler = new(ByteSize)
40+
_ yaml.Unmarshaler = new(ByteSize)
41+
)
42+
43+
func parseFileSizeInt(s string) (uint64, error) {
44+
// os.FileInfo reports size as int64, don't support bigger numbers.
45+
maxBitSize := 63
46+
return strconv.ParseUint(s, 10, maxBitSize)
47+
}
48+
49+
// MarshalJSON implements the json.Marshaler interface for FileSize, it returns
50+
// the string representation in a format that can be unmarshaled back to an
51+
// equivalent value.
52+
func (s ByteSize) MarshalJSON() ([]byte, error) {
53+
return []byte(`"` + s.String() + `"`), nil
54+
}
55+
56+
// MarshalYAML implements the yaml.Marshaler interface for FileSize, it returns
57+
// the string representation in a format that can be unmarshaled back to an
58+
// equivalent value.
59+
func (s ByteSize) MarshalYAML() (interface{}, error) {
60+
return s.String(), nil
61+
}
62+
63+
// UnmarshalJSON implements the json.Unmarshaler interface for FileSize.
64+
func (s *ByteSize) UnmarshalJSON(d []byte) error {
65+
// Support unquoted plain numbers.
66+
bytes, err := parseFileSizeInt(string(d))
67+
if err == nil {
68+
*s = ByteSize(bytes)
69+
return nil
70+
}
71+
72+
var text string
73+
err = json.Unmarshal(d, &text)
74+
if err != nil {
75+
return err
76+
}
77+
78+
return s.unmarshalString(text)
79+
}
80+
81+
// UnmarshalYAML implements the yaml.Unmarshaler interface for FileSize.
82+
func (s *ByteSize) UnmarshalYAML(value *yaml.Node) error {
83+
// Support unquoted plain numbers.
84+
bytes, err := parseFileSizeInt(value.Value)
85+
if err == nil {
86+
*s = ByteSize(bytes)
87+
return nil
88+
}
89+
90+
return s.unmarshalString(value.Value)
91+
}
92+
93+
var bytesPattern = regexp.MustCompile(fmt.Sprintf(`^(\d+(\.\d+)?)(%s|%s|%s|%s|)$`, byteString, kiloByteString, megaByteString, gigaByteString))
94+
95+
func (s *ByteSize) unmarshalString(text string) error {
96+
match := bytesPattern.FindStringSubmatch(text)
97+
if len(match) < 3 {
98+
return fmt.Errorf("invalid format for size in bytes (%s)", text)
99+
}
100+
101+
if match[2] == "" {
102+
q, err := parseFileSizeInt(match[1])
103+
if err != nil {
104+
return fmt.Errorf("invalid format for size in bytes (%s): %w", text, err)
105+
}
106+
107+
unit := match[3]
108+
switch unit {
109+
case gigaByteString:
110+
*s = ByteSize(q) * GigaByte
111+
case megaByteString:
112+
*s = ByteSize(q) * MegaByte
113+
case kiloByteString:
114+
*s = ByteSize(q) * KiloByte
115+
case byteString, "":
116+
*s = ByteSize(q) * Byte
117+
default:
118+
return fmt.Errorf("invalid unit for filesize (%s): %s", text, unit)
119+
}
120+
} else {
121+
q, err := strconv.ParseFloat(match[1], 64)
122+
if err != nil {
123+
return fmt.Errorf("invalid format for size in bytes (%s): %w", text, err)
124+
}
125+
126+
unit := match[3]
127+
switch unit {
128+
case gigaByteString:
129+
*s = approxFloat(q, GigaByte)
130+
case megaByteString:
131+
*s = approxFloat(q, MegaByte)
132+
case kiloByteString:
133+
*s = approxFloat(q, KiloByte)
134+
case byteString, "":
135+
*s = approxFloat(q, Byte)
136+
default:
137+
return fmt.Errorf("invalid unit for filesize (%s): %s", text, unit)
138+
}
139+
}
140+
141+
return nil
142+
}
143+
144+
func approxFloat(n float64, unit ByteSize) ByteSize {
145+
approx := n * float64(unit)
146+
return ByteSize(math.Round(approx))
147+
}
148+
149+
// String returns the string representation of the FileSize.
150+
func (s ByteSize) String() string {
151+
format := func(q ByteSize, unit string) string {
152+
return fmt.Sprintf("%d%s", q, unit)
153+
}
154+
155+
if s >= GigaByte && (s%GigaByte == 0) {
156+
return format(s/GigaByte, gigaByteString)
157+
}
158+
159+
if s >= MegaByte && (s%MegaByte == 0) {
160+
return format(s/MegaByte, megaByteString)
161+
}
162+
163+
if s >= KiloByte && (s%KiloByte == 0) {
164+
return format(s/KiloByte, kiloByteString)
165+
}
166+
167+
return format(s, byteString)
168+
}

internal/common/bytesize_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package common
6+
7+
import (
8+
"encoding/json"
9+
"testing"
10+
11+
"gopkg.in/yaml.v3"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestFileSizeMarshallJSON(t *testing.T) {
18+
cases := []struct {
19+
fileSize ByteSize
20+
expected string
21+
}{
22+
{ByteSize(0), `"0B"`},
23+
{ByteSize(1024), `"1KB"`},
24+
{ByteSize(1025), `"1025B"`},
25+
{5 * MegaByte, `"5MB"`},
26+
{5 * GigaByte, `"5GB"`},
27+
}
28+
29+
for _, c := range cases {
30+
t.Run(c.expected, func(t *testing.T) {
31+
d, err := json.Marshal(c.fileSize)
32+
require.NoError(t, err)
33+
assert.Equal(t, c.expected, string(d))
34+
})
35+
}
36+
}
37+
38+
func TestFileSizeMarshallYAML(t *testing.T) {
39+
cases := []struct {
40+
fileSize ByteSize
41+
expected string
42+
}{
43+
{ByteSize(0), "0B\n"},
44+
{ByteSize(1024), "1KB\n"},
45+
{ByteSize(1025), "1025B\n"},
46+
{5 * MegaByte, "5MB\n"},
47+
{5 * GigaByte, "5GB\n"},
48+
}
49+
50+
for _, c := range cases {
51+
t.Run(c.expected, func(t *testing.T) {
52+
d, err := yaml.Marshal(c.fileSize)
53+
require.NoError(t, err)
54+
assert.Equal(t, c.expected, string(d))
55+
})
56+
}
57+
}
58+
59+
func TestFileSizeUnmarshal(t *testing.T) {
60+
t.Run("json", func(t *testing.T) {
61+
testFileSizeUnmarshalFormat(t, json.Unmarshal)
62+
})
63+
t.Run("yaml", func(t *testing.T) {
64+
testFileSizeUnmarshalFormat(t, yaml.Unmarshal)
65+
})
66+
}
67+
68+
func testFileSizeUnmarshalFormat(t *testing.T, unmarshaler func([]byte, interface{}) error) {
69+
cases := []struct {
70+
json string
71+
expected ByteSize
72+
valid bool
73+
}{
74+
{"0", 0, true},
75+
{"1024", 1024 * Byte, true},
76+
{`"1024"`, 1024 * Byte, true},
77+
{`"1024B"`, 1024 * Byte, true},
78+
{`"10MB"`, 10 * MegaByte, true},
79+
{`"40GB"`, 40 * GigaByte, true},
80+
{`"56.21GB"`, approxFloat(56.21, GigaByte), true},
81+
{`"2KB"`, 2 * KiloByte, true},
82+
{`"KB"`, 0, false},
83+
{`"1s"`, 0, false},
84+
{`""`, 0, false},
85+
{`"B"`, 0, false},
86+
{`"-200MB"`, 0, false},
87+
{`"-1"`, 0, false},
88+
{`"10000000000000000000MB"`, 0, false},
89+
}
90+
91+
for _, c := range cases {
92+
t.Run(c.json, func(t *testing.T) {
93+
var found ByteSize
94+
err := unmarshaler([]byte(c.json), &found)
95+
if c.valid {
96+
require.NoError(t, err)
97+
assert.Equal(t, c.expected, found)
98+
} else {
99+
require.Error(t, err)
100+
}
101+
})
102+
}
103+
}

internal/compose/compose.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"os"
1515
"os/exec"
1616
"regexp"
17+
"slices"
1718
"strconv"
1819
"strings"
1920
"time"
@@ -60,7 +61,23 @@ type Config struct {
6061
Services map[string]service
6162
}
6263

64+
// Images lists the images found in the configuration.
65+
func (c *Config) Images() []string {
66+
var images []string
67+
for _, service := range c.Services {
68+
if service.Image == "" {
69+
continue
70+
}
71+
if slices.Contains(images, service.Image) {
72+
continue
73+
}
74+
images = append(images, service.Image)
75+
}
76+
return images
77+
}
78+
6379
type service struct {
80+
Image string
6481
Ports []portMapping
6582
Environment map[string]string
6683
}

0 commit comments

Comments
 (0)