Skip to content

Commit 305fa43

Browse files
authored
Support caching kernels and boot images. (#6)
1 parent 9d3f146 commit 305fa43

File tree

22 files changed

+1589
-664
lines changed

22 files changed

+1589
-664
lines changed

.github/workflows/latest.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Lint
2323
uses: golangci/golangci-lint-action@v2
2424
with:
25-
version: v1.32.2
25+
args: -p bugs -p unused --timeout=3m
2626

2727
- name: Build and push Docker image
2828
run: |

.github/workflows/pull_request.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Lint
2828
uses: golangci/golangci-lint-action@v2
2929
with:
30-
version: v1.32.2
30+
args: -p bugs -p unused --timeout=3m
3131

3232
- name: Build Docker image
3333
run: |

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Lint
2323
uses: golangci/golangci-lint-action@v2
2424
with:
25-
version: v1.32.2
25+
args: -p bugs -p unused --timeout=3m
2626

2727
- name: Build and push Docker image
2828
run: |

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM metalstack/builder:latest as builder
22

3-
FROM alpine:3.12
3+
FROM alpine:3.13
44
RUN apk add --no-cache tini ca-certificates
55
COPY --from=builder /work/bin/metal-image-cache-sync /metal-image-cache-sync
66
CMD ["/metal-image-cache-sync"]

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ release:: all;
1616

1717
.PHONY: start
1818
start: all
19+
mkdir -p /tmp/metal-image-cache
1920
bin/metal-image-cache-sync \
20-
# --log-level debug \
21+
--log-level debug \
2122
--metal-api-endpoint $(METAL_API_ENDPOINT) \
2223
--metal-api-hmac $(METAL_API_HMAC) \
2324
--max-cache-size 10G \
2425
--min-images-per-name 2 \
25-
--root-path /tmp/metal-image-cache \
26-
--dry-run
26+
--cache-root-path /tmp/metal-image-cache \
27+
--enable-kernel-cache \
28+
--enable-boot-image-cache \
29+
# --dry-run

cmd/internal/determine-sync-images/lister.go

Lines changed: 171 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package synclister
22

33
import (
4+
"context"
45
"fmt"
6+
"net/http"
57
"net/url"
68
"sort"
9+
"strconv"
710
"strings"
811
"time"
912

@@ -17,24 +20,28 @@ import (
1720
)
1821

1922
type SyncLister struct {
20-
logger *zap.SugaredLogger
21-
driver *metalgo.Driver
22-
config *api.Config
23-
s3 *s3.S3
24-
collector *metrics.Collector
23+
logger *zap.SugaredLogger
24+
driver *metalgo.Driver
25+
config *api.Config
26+
s3 *s3.S3
27+
stop context.Context
28+
imageCollector *metrics.ImageCollector
29+
httpClient *http.Client
2530
}
2631

27-
func NewSyncLister(logger *zap.SugaredLogger, driver *metalgo.Driver, s3 *s3.S3, collector *metrics.Collector, config *api.Config) *SyncLister {
32+
func NewSyncLister(logger *zap.SugaredLogger, driver *metalgo.Driver, s3 *s3.S3, imageCollector *metrics.ImageCollector, config *api.Config, stop context.Context) *SyncLister {
2833
return &SyncLister{
29-
logger: logger,
30-
driver: driver,
31-
config: config,
32-
s3: s3,
33-
collector: collector,
34+
logger: logger,
35+
driver: driver,
36+
config: config,
37+
s3: s3,
38+
stop: stop,
39+
imageCollector: imageCollector,
40+
httpClient: http.DefaultClient,
3441
}
3542
}
3643

37-
func (s *SyncLister) DetermineSyncList() ([]api.OS, error) {
44+
func (s *SyncLister) DetermineImageSyncList() ([]api.OS, error) {
3845
s3Images, err := s.retrieveImagesFromS3()
3946
if err != nil {
4047
return nil, errors.Wrap(err, "error listing images in s3")
@@ -45,21 +52,13 @@ func (s *SyncLister) DetermineSyncList() ([]api.OS, error) {
4552
return nil, errors.Wrap(err, "error listing images")
4653
}
4754

48-
s.collector.SetMetalAPIImageCount(len(resp.Image))
55+
s.imageCollector.SetMetalAPIImageCount(len(resp.Image))
4956

5057
expirationGraceDays := 24 * time.Hour * time.Duration(s.config.ExpirationGraceDays)
5158

5259
images := api.OSImagesByOS{}
5360
for _, img := range resp.Image {
54-
skip := false
55-
for _, exclude := range s.config.ExcludePaths {
56-
if strings.Contains(img.URL, exclude) {
57-
skip = true
58-
break
59-
}
60-
}
61-
62-
if skip {
61+
if s.isExcluded(img.URL) {
6362
s.logger.Debugw("skipping image with exclude URL", "id", *img.ID)
6463
continue
6564
}
@@ -106,12 +105,13 @@ func (s *SyncLister) DetermineSyncList() ([]api.OS, error) {
106105
}
107106

108107
imageVersions = append(imageVersions, api.OS{
109-
Name: os,
110-
Version: ver,
111-
ApiRef: *img,
112-
BucketKey: bucketKey,
113-
ImageRef: s3Image,
114-
MD5Ref: s3MD5,
108+
Name: os,
109+
Version: ver,
110+
ApiRef: *img,
111+
BucketKey: bucketKey,
112+
BucketName: s.config.ImageBucket,
113+
ImageRef: s3Image,
114+
MD5Ref: s3MD5,
115115
})
116116

117117
versions[majorMinor] = imageVersions
@@ -121,12 +121,13 @@ func (s *SyncLister) DetermineSyncList() ([]api.OS, error) {
121121
var sizeCount int64
122122
var syncImages []api.OS
123123
for _, versions := range images {
124-
for _, images := range versions {
125-
sort.Slice(images, func(i, j int) bool {
126-
return images[i].Version.GreaterThan(images[j].Version)
124+
for _, versionedImages := range versions {
125+
versionedImages := versionedImages
126+
sort.Slice(versionedImages, func(i, j int) bool {
127+
return versionedImages[i].Version.GreaterThan(versionedImages[j].Version)
127128
})
128129
amount := 0
129-
for _, img := range images {
130+
for _, img := range versionedImages {
130131
if s.config.MaxImagesPerName > 0 && amount >= s.config.MaxImagesPerName {
131132
break
132133
}
@@ -151,11 +152,148 @@ func (s *SyncLister) DetermineSyncList() ([]api.OS, error) {
151152
}
152153
}
153154

154-
s.collector.SetUnsyncedImageCount(len(resp.Image) - len(syncImages))
155+
s.imageCollector.SetUnsyncedImageCount(len(resp.Image) - len(syncImages))
155156

156157
return syncImages, nil
157158
}
158159

160+
func (s *SyncLister) isExcluded(url string) bool {
161+
for _, exclude := range s.config.ExcludePaths {
162+
if strings.Contains(url, exclude) {
163+
return true
164+
}
165+
}
166+
167+
return false
168+
}
169+
170+
func (s *SyncLister) DetermineKernelSyncList() ([]api.Kernel, error) {
171+
resp, err := s.driver.PartitionList()
172+
if err != nil {
173+
return nil, errors.Wrap(err, "error listing partitions")
174+
}
175+
176+
var result []api.Kernel
177+
urls := map[string]bool{}
178+
179+
for _, p := range resp.Partition {
180+
if p.Bootconfig == nil {
181+
continue
182+
}
183+
184+
kernelURL := p.Bootconfig.Kernelurl
185+
186+
if urls[kernelURL] {
187+
continue
188+
}
189+
190+
if s.isExcluded(kernelURL) {
191+
s.logger.Debugw("skipping kernel with exclude URL", "url", kernelURL)
192+
continue
193+
}
194+
195+
u, err := url.Parse(kernelURL)
196+
if err != nil {
197+
s.logger.Errorw("kernel url is invalid, skipping", "error", err)
198+
continue
199+
}
200+
201+
size, err := retrieveContentLength(s.stop, s.httpClient, u.String())
202+
if err != nil {
203+
s.logger.Warnw("unable to determine kernel download size", "error", err)
204+
}
205+
206+
result = append(result, api.Kernel{
207+
SubPath: strings.TrimPrefix(u.Path, "/"),
208+
URL: kernelURL,
209+
Size: size,
210+
})
211+
urls[kernelURL] = true
212+
}
213+
214+
return result, nil
215+
}
216+
217+
func (s *SyncLister) DetermineBootImageSyncList() ([]api.BootImage, error) {
218+
resp, err := s.driver.PartitionList()
219+
if err != nil {
220+
return nil, errors.Wrap(err, "error listing partitions")
221+
}
222+
223+
var result []api.BootImage
224+
urls := map[string]bool{}
225+
226+
for _, p := range resp.Partition {
227+
if p.Bootconfig == nil {
228+
continue
229+
}
230+
231+
bootImageURL := p.Bootconfig.Imageurl
232+
233+
if urls[bootImageURL] {
234+
continue
235+
}
236+
237+
if s.isExcluded(bootImageURL) {
238+
s.logger.Debugw("skipping boot image with exclude URL", "url", bootImageURL)
239+
continue
240+
}
241+
242+
u, err := url.Parse(bootImageURL)
243+
if err != nil {
244+
s.logger.Errorw("boot image url is invalid, skipping", "error", err)
245+
continue
246+
}
247+
248+
size, err := retrieveContentLength(s.stop, s.httpClient, u.String())
249+
if err != nil {
250+
s.logger.Warnw("unable to determine boot image download size", "error", err)
251+
}
252+
253+
md5URL := u.String() + ".md5"
254+
_, err = retrieveContentLength(s.stop, s.httpClient, md5URL)
255+
if err != nil {
256+
s.logger.Errorw("boot image md5 does not exist, skipping", "url", md5URL, "error", err)
257+
continue
258+
}
259+
260+
result = append(result, api.BootImage{
261+
SubPath: strings.TrimPrefix(u.Path, "/"),
262+
URL: bootImageURL,
263+
Size: size,
264+
})
265+
urls[bootImageURL] = true
266+
}
267+
268+
return result, nil
269+
}
270+
271+
func retrieveContentLength(ctx context.Context, c *http.Client, url string) (int64, error) {
272+
req, err := http.NewRequest(http.MethodHead, url, nil)
273+
if err != nil {
274+
return 0, errors.Wrap(err, "unable to create head request")
275+
}
276+
277+
req = req.WithContext(ctx)
278+
279+
resp, err := c.Do(req)
280+
if err != nil {
281+
return 0, err
282+
}
283+
defer resp.Body.Close()
284+
285+
if resp.StatusCode != http.StatusOK {
286+
return 0, fmt.Errorf("head request to url did not return OK: %s", url)
287+
}
288+
289+
size, err := strconv.Atoi(resp.Header.Get("Content-Length"))
290+
if err != nil {
291+
return 0, errors.Wrap(err, "content-length header value could not be converted to integer")
292+
}
293+
294+
return int64(size), nil
295+
}
296+
159297
func (s *SyncLister) reduce(images []api.OS, sizeCount int64) ([]api.OS, int64, error) {
160298
groups := map[string][]api.OS{}
161299
for _, img := range images {

0 commit comments

Comments
 (0)