From ccfa00d118d93f377edd88bf557831c73e6410b2 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Wed, 21 Feb 2024 15:23:59 +0100 Subject: [PATCH] Add purge support (#4) --- README.md | 5 +- api/v1/types.go | 26 +++- api/v1/types_test.go | 31 +++- cmd/main.go | 82 +++++++++- cmd/server.go | 30 +++- deploy/oci-mirror.yaml | 62 ++++++++ go.mod | 52 ++++--- go.sum | 114 ++++++++------ oci-mirror.yaml | 10 ++ pkg/container/auth.go | 37 +++++ pkg/container/mirror.go | 96 ++++++++++++ pkg/{mirror => container}/mirror_test.go | 15 +- pkg/container/purge.go | 162 +++++++++++++++++++ pkg/container/purge_test.go | 157 +++++++++++++++++++ pkg/container/registry.go | 36 +++++ pkg/container/tags.go | 125 +++++++++++++++ pkg/mirror/mirror.go | 189 ----------------------- 17 files changed, 962 insertions(+), 267 deletions(-) create mode 100644 pkg/container/auth.go create mode 100644 pkg/container/mirror.go rename pkg/{mirror => container}/mirror_test.go (93%) create mode 100644 pkg/container/purge.go create mode 100644 pkg/container/purge_test.go create mode 100644 pkg/container/registry.go create mode 100644 pkg/container/tags.go delete mode 100644 pkg/mirror/mirror.go diff --git a/README.md b/README.md index f2ca302..dc4b3ea 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ kubectl apply -f deploy ## TODO +- [x] support purging - [ ] eventually support http(s) artifacts to be stored as OCIs -- [ ] support Regex Match for image tags -- [ ] store a OCI artifact which reflects all stored images +- [ ] ~~~support Regex Match for image tags~~~ +- [ ] store a OCI artifact which reflects all stored images ? diff --git a/api/v1/types.go b/api/v1/types.go index 43d1bc9..c2a5e93 100644 --- a/api/v1/types.go +++ b/api/v1/types.go @@ -37,6 +37,8 @@ type ImageMirror struct { Destination string `json:"destination,omitempty"` // Match defines which images to mirror Match Match `json:"match,omitempty"` + // Purge defines which images should be purged + Purge *Purge `json:"purge,omitempty"` } type Match struct { @@ -50,6 +52,16 @@ type Match struct { Last *int64 `json:"last,omitempty"` } +type Purge struct { + // Tags is a exact list of tags to purge + Tags []string `json:"tags,omitempty"` + // Semver defines a semantic version of tags to purge + Semver *string `json:"semver,omitempty"` + // NoMatch if set to true, all images which are not matched by the Match specification will be purged. + // latest will never be purged + NoMatch bool `json:"no_match,omitempty"` +} + func (c Config) Validate() error { var errs []error sources := make(map[string]bool) @@ -92,12 +104,22 @@ func (c Config) Validate() error { } if image.Match.Semver != nil { - _, err := semver.NewConstraint(*image.Match.Semver) - if err != nil { + if _, err := semver.NewConstraint(*image.Match.Semver); err != nil { errs = append(errs, fmt.Errorf("image.match.semver is invalid, image source:%q, semver:%q %w", image.Source, *image.Match.Semver, err)) } } + if image.Purge != nil { + if image.Purge.Semver != nil { + if _, err := semver.NewConstraint(*image.Purge.Semver); err != nil { + errs = append(errs, fmt.Errorf("image.purge.semver is invalid, image source:%q, semver:%q %w", image.Source, *image.Purge.Semver, err)) + } + } + if image.Purge.NoMatch && image.Match.AllTags { + errs = append(errs, fmt.Errorf("image.purge.nomatch and image.match.alltags cannot be set both image source:%q", image.Source)) + } + } + srcRef, err := name.ParseReference(image.Source) if err != nil { errs = append(errs, err) diff --git a/api/v1/types_test.go b/api/v1/types_test.go index 5163bd5..7078a92 100644 --- a/api/v1/types_test.go +++ b/api/v1/types_test.go @@ -52,7 +52,7 @@ func TestConfig_Validate(t *testing.T) { wantErr: true, }, { - name: "invalid semver", + name: "invalid match semver", Images: []ImageMirror{ { Source: "abc", @@ -64,6 +64,19 @@ func TestConfig_Validate(t *testing.T) { }, wantErr: true, }, + { + name: "invalid purge semver", + Images: []ImageMirror{ + { + Source: "abc", + Destination: "abc", + Purge: &Purge{ + Semver: pointer.Pointer("abc"), + }, + }, + }, + wantErr: true, + }, { name: "image cde is used in two images", Images: []ImageMirror{ @@ -108,6 +121,22 @@ func TestConfig_Validate(t *testing.T) { }, wantErr: true, }, + { + name: "invalid purge and alltags set", + Images: []ImageMirror{ + { + Source: "abc", + Destination: "abc", + Match: Match{ + AllTags: true, + }, + Purge: &Purge{ + NoMatch: true, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/main.go b/cmd/main.go index 92feea4..50da611 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -57,13 +57,91 @@ var ( } s := newServer(log, config) - if err := s.run(); err != nil { + if err := s.mirror(); err != nil { log.Error("error during mirror", "error", err) os.Exit(1) } return nil }, } + purgeCmd = &cli.Command{ + Name: "purge", + Usage: "purge images as specified in configuration", + Flags: []cli.Flag{ + debugFlag, + configMapFlag, + }, + Action: func(ctx *cli.Context) error { + level := slog.LevelInfo + if ctx.Bool(debugFlag.Name) { + level = slog.LevelDebug + } + jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) + log := slog.New(jsonHandler) + + log.Info("start purge", "version", v.V.String()) + raw, err := os.ReadFile(ctx.String(configMapFlag.Name)) + if err != nil { + return fmt.Errorf("unable to read config file:%w", err) + } + var config apiv1.Config + err = yaml.Unmarshal(raw, &config) + if err != nil { + return fmt.Errorf("unable to parse config file:%w", err) + } + + err = config.Validate() + if err != nil { + return fmt.Errorf("config invalid:%w", err) + } + + s := newServer(log, config) + if err := s.purge(); err != nil { + log.Error("error during purge", "error", err) + os.Exit(1) + } + return nil + }, + } + purgeUnknwonCmd = &cli.Command{ + Name: "purge-unknown", + Usage: "purge unknown images according to the configuration", + Flags: []cli.Flag{ + debugFlag, + configMapFlag, + }, + Action: func(ctx *cli.Context) error { + level := slog.LevelInfo + if ctx.Bool(debugFlag.Name) { + level = slog.LevelDebug + } + jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) + log := slog.New(jsonHandler) + + log.Info("start purge unknown", "version", v.V.String()) + raw, err := os.ReadFile(ctx.String(configMapFlag.Name)) + if err != nil { + return fmt.Errorf("unable to read config file:%w", err) + } + var config apiv1.Config + err = yaml.Unmarshal(raw, &config) + if err != nil { + return fmt.Errorf("unable to parse config file:%w", err) + } + + err = config.Validate() + if err != nil { + return fmt.Errorf("config invalid:%w", err) + } + + s := newServer(log, config) + if err := s.purgeUnknown(); err != nil { + log.Error("error during purge", "error", err) + os.Exit(1) + } + return nil + }, + } ) func main() { @@ -72,6 +150,8 @@ func main() { Usage: "oci mirror server", Commands: []*cli.Command{ mirrorCmd, + purgeCmd, + purgeUnknwonCmd, }, } diff --git a/cmd/server.go b/cmd/server.go index 5e1ec61..b4055ff 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -7,7 +7,7 @@ import ( "time" apiv1 "github.com/metal-stack/oci-mirror/api/v1" - "github.com/metal-stack/oci-mirror/pkg/mirror" + "github.com/metal-stack/oci-mirror/pkg/container" ) type server struct { @@ -22,9 +22,9 @@ func newServer(log *slog.Logger, config apiv1.Config) *server { } } -func (s *server) run() error { +func (s *server) mirror() error { start := time.Now() - m := mirror.New(s.log, s.config) + m := container.New(s.log.WithGroup("mirror"), s.config) err := m.Mirror(context.Background()) if err != nil { s.log.Error(fmt.Sprintf("error mirroring images, duration %s", time.Since(start)), "error", err) @@ -33,3 +33,27 @@ func (s *server) run() error { s.log.Info(fmt.Sprintf("finished mirroring after %s", time.Since(start))) return nil } + +func (s *server) purge() error { + start := time.Now() + m := container.New(s.log.WithGroup("purge"), s.config) + err := m.Purge(context.Background()) + if err != nil { + s.log.Error(fmt.Sprintf("error purging images, duration %s", time.Since(start)), "error", err) + return err + } + s.log.Info(fmt.Sprintf("finished purging after %s", time.Since(start))) + return nil +} + +func (s *server) purgeUnknown() error { + start := time.Now() + m := container.New(s.log.WithGroup("purgeunknown"), s.config) + err := m.PurgeUnknown(context.Background()) + if err != nil { + s.log.Error(fmt.Sprintf("error purging unknown images, duration %s", time.Since(start)), "error", err) + return err + } + s.log.Info(fmt.Sprintf("finished purging unknown after %s", time.Since(start))) + return nil +} diff --git a/deploy/oci-mirror.yaml b/deploy/oci-mirror.yaml index ca26758..d4332a4 100644 --- a/deploy/oci-mirror.yaml +++ b/deploy/oci-mirror.yaml @@ -34,6 +34,68 @@ spec: path: oci-mirror.yaml restartPolicy: OnFailure --- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: oci-mirror-purge + namespace: mirror +spec: + schedule: "*/40 * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: oci-mirror + image: ghcr.io/metal-stack/oci-mirror + imagePullPolicy: IfNotPresent + args: + - purge + - --mirror-config=/config/oci-mirror.yaml + volumeMounts: + - name: mirror-config + mountPath: /config + volumes: + - name: mirror-config + secret: + secretName: mirror-config + items: + - key: oci-mirror.yaml + path: oci-mirror.yaml + restartPolicy: OnFailure +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: oci-mirror-purge-unknown + namespace: mirror +spec: + # once a week on every monday at 2:00 o'clock + schedule: "0 2 * * 1" + jobTemplate: + spec: + template: + spec: + containers: + - name: oci-mirror + image: ghcr.io/metal-stack/oci-mirror + imagePullPolicy: IfNotPresent + args: + - purge-unknown + - --mirror-config=/config/oci-mirror.yaml + volumeMounts: + - name: mirror-config + mountPath: /config + volumes: + - name: mirror-config + secret: + secretName: mirror-config + items: + - key: oci-mirror.yaml + path: oci-mirror.yaml + restartPolicy: OnFailure + +--- apiVersion: v1 kind: Secret metadata: diff --git a/go.mod b/go.mod index 369f2ef..8e2bb14 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/metal-stack/oci-mirror -go 1.21 +go 1.22 require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c - github.com/google/go-containerregistry v0.17.0 - github.com/metal-stack/metal-lib v0.14.2 + github.com/google/go-containerregistry v0.19.0 + github.com/metal-stack/metal-lib v0.14.4 github.com/metal-stack/v v1.0.3 github.com/stretchr/testify v1.8.4 - github.com/testcontainers/testcontainers-go v0.27.0 + github.com/testcontainers/testcontainers-go v0.28.0 github.com/urfave/cli/v2 v2.27.1 sigs.k8s.io/yaml v1.4.0 ) @@ -21,55 +21,63 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/containerd/containerd v1.7.11 // indirect + github.com/containerd/containerd v1.7.13 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/cli v24.0.7+incompatible // indirect + github.com/docker/cli v25.0.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v24.0.7+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/docker/docker v25.0.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.1 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc5 // indirect - github.com/opencontainers/runc v1.1.11 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect + github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shirou/gopsutil/v3 v3.24.1 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/vbatts/tar-split v0.11.5 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect - golang.org/x/mod v0.14.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect + go.opentelemetry.io/otel v1.23.1 // indirect + go.opentelemetry.io/otel/metric v1.23.1 // indirect + go.opentelemetry.io/otel/trace v1.23.1 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mod v0.15.0 // indirect golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/tools v0.16.1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/grpc v1.60.1 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/grpc v1.61.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3e9f9ff..6a1a634 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7 github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= -github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= +github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is= +github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= @@ -34,20 +34,27 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= -github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6/EsX/6284= +github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= -github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= +github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= +github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -61,14 +68,16 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk= -github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic= +github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -78,8 +87,8 @@ github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIg github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/metal-stack/metal-lib v0.14.2 h1:ntIZiV8fVoWsgPLXOy9xrObZr1NdU5caYUP0zzefUME= -github.com/metal-stack/metal-lib v0.14.2/go.mod h1:2wKxFXSCpA1Dr+Rq0ddpQCPKPGMWJp4cpIaVTM4lDi0= +github.com/metal-stack/metal-lib v0.14.4 h1:vm2868vcua6khoyWL7d0to8Hq5RayrjMse0FZTyWEec= +github.com/metal-stack/metal-lib v0.14.4/go.mod h1:Z3PAh8dkyWC4B19fXsu6EYwXXee0Lk9JZbjoHPLbDbc= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -88,16 +97,16 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v1.1.11 h1:9LjxyVlE0BPMRP2wuQDRlHV4941Jp9rc3F0+YKimopA= -github.com/opencontainers/runc v1.1.11/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -105,14 +114,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= -github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a h1:XCUtNgBnZfUBhdfCX2QK+fslr9vevSsUg3W3peZwlak= +github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= -github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= +github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -127,8 +136,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/testcontainers/testcontainers-go v0.27.0 h1:IeIrJN4twonTDuMuBNQdKZ+K97yd7VrmNGu+lDpYcDk= -github.com/testcontainers/testcontainers-go v0.27.0/go.mod h1:+HgYZcd17GshBUZv9b+jKFJ198heWPQq3KQIp2+N+7U= +github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8= +github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= @@ -143,26 +152,43 @@ github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7v github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY= +go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo= +go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8= +go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= -golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -178,9 +204,9 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -191,16 +217,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= diff --git a/oci-mirror.yaml b/oci-mirror.yaml index e2b3131..1013d66 100644 --- a/oci-mirror.yaml +++ b/oci-mirror.yaml @@ -21,11 +21,21 @@ images: tags: - "3.17" - "3.18" + # purge defines which tags should be purged, optional + purge: + # semver spec of tags to purge of this image + semver: "<= 3.16" + # tags to purge + tags: + - "foo" - source: "busybox" destination: "172.17.0.1:5000/library/busybox" match: # semver will only mirror the tags of the source images which match this semantic version constraint semver: ">= 1.35" + purge: + # no_match will purge all images which are not matched with the above match spec, latest will never be purged + no_match: true - source: "nginx" destination: "172.17.0.1:5000/library/nginx" match: diff --git a/pkg/container/auth.go b/pkg/container/auth.go new file mode 100644 index 0000000..8380c13 --- /dev/null +++ b/pkg/container/auth.go @@ -0,0 +1,37 @@ +package container + +import ( + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + apiv1 "github.com/metal-stack/oci-mirror/api/v1" +) + +func (m *mirror) ensureAuthOption(image *apiv1.ImageMirror) ([]crane.Option, error) { + var opts []crane.Option + if image == nil { + return opts, fmt.Errorf("image is nil") + } + if strings.HasPrefix(image.Destination, "http://") { + opts = append(opts, crane.Insecure) + image.Destination = strings.ReplaceAll(image.Destination, "http://", "") + } + dstRef, err := name.ParseReference(image.Destination) + if err != nil { + return opts, err + } + registryName := dstRef.Context().Registry.Name() + registry, ok := m.config.Registries[registryName] + if !ok { + return opts, nil + } + auth := crane.WithAuth(&authn.Basic{ + Username: registry.Auth.Username, + Password: registry.Auth.Password, + }) + opts = append(opts, auth) + return opts, nil +} diff --git a/pkg/container/mirror.go b/pkg/container/mirror.go new file mode 100644 index 0000000..3dc69ac --- /dev/null +++ b/pkg/container/mirror.go @@ -0,0 +1,96 @@ +package container + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + + apiv1 "github.com/metal-stack/oci-mirror/api/v1" +) + +type mirror struct { + log *slog.Logger + config apiv1.Config +} + +func New(log *slog.Logger, config apiv1.Config) *mirror { + return &mirror{ + log: log, + config: config, + } +} + +func (m *mirror) Mirror(ctx context.Context) error { + var ( + errs []error + ) + for _, image := range m.config.Images { + image := image + var ( + err error + opts []crane.Option + ) + + opts, err = m.ensureAuthOption(&image) + if err != nil { + m.log.Warn("unable detect auth, continue unauthenticated", "error", err) + } + opts = append(opts, crane.WithContext(ctx)) + + m.log.Info("consider mirror from", "source", image.Source, "destination", image.Destination) + + if image.Match.AllTags { + m.log.Info("mirror all tags from", "source", image.Source, "destination", image.Destination) + err := crane.CopyRepository(image.Source, image.Destination, opts...) + if err != nil { + m.log.Error("unable to copy all images", "image", image.Source, "error", err) + errs = append(errs, err) + } + continue + } + + tagsToCopy, err := m.getTagsToCopy(image, opts) + if err != nil { + errs = append(errs, err) + continue + } + + for src, dst := range tagsToCopy { + if !strings.HasSuffix(dst, ":latest") { + opts = append(opts, crane.WithNoClobber(false)) + } + m.log.Info("mirror from", "source", src, "destination", dst) + rawmanifest, err := crane.Manifest(src, opts...) + if err != nil { + m.log.Error("unable to read image manifest", "error", err) + errs = append(errs, err) + continue + } + manifest := v1.Manifest{} + if err := json.Unmarshal(rawmanifest, &manifest); err != nil { + m.log.Error("unable to decode image manifest", "error", err) + errs = append(errs, err) + continue + } + if manifest.SchemaVersion < 2 { + m.log.Warn("image manifest scheme version to low, ignoring", "image", src, "scheme version", manifest.SchemaVersion) + continue + } + err = crane.Copy(src, dst, opts...) + if err != nil { + m.log.Error("unable to copy", "source", src, "dst", dst, "error", err) + errs = append(errs, err) + } + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} diff --git a/pkg/mirror/mirror_test.go b/pkg/container/mirror_test.go similarity index 93% rename from pkg/mirror/mirror_test.go rename to pkg/container/mirror_test.go index d2983ca..c7d8b50 100644 --- a/pkg/mirror/mirror_test.go +++ b/pkg/container/mirror_test.go @@ -1,7 +1,8 @@ -package mirror_test +package container_test import ( "context" + "crypto/rand" "fmt" "log/slog" "os" @@ -12,7 +13,7 @@ import ( "github.com/google/go-containerregistry/pkg/crane" "github.com/metal-stack/metal-lib/pkg/pointer" apiv1 "github.com/metal-stack/oci-mirror/api/v1" - "github.com/metal-stack/oci-mirror/pkg/mirror" + "github.com/metal-stack/oci-mirror/pkg/container" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -105,7 +106,7 @@ func TestMirror(t *testing.T) { }, } - m := mirror.New(slog.Default(), config) + m := container.New(slog.Default(), config) err = m.Mirror(context.Background()) require.NoError(t, err) @@ -170,7 +171,13 @@ func startRegistry(env map[string]string, src, dst *string) (string, int, error) } func createImage(name string, tags ...string) error { - img, err := crane.Image(map[string][]byte{}) + // ensure every image has distinct content + buf := make([]byte, 128) + _, err := rand.Read(buf) + if err != nil { + return err + } + img, err := crane.Image(map[string][]byte{"a": buf}) if err != nil { return err } diff --git a/pkg/container/purge.go b/pkg/container/purge.go new file mode 100644 index 0000000..2ad54d5 --- /dev/null +++ b/pkg/container/purge.go @@ -0,0 +1,162 @@ +package container + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" +) + +func (m *mirror) Purge(ctx context.Context) error { + var ( + errs []error + ) + for _, image := range m.config.Images { + image := image + if image.Purge == nil { + continue + } + + var ( + err error + opts []crane.Option + tagsToPurge []string + ) + + opts, err = m.ensureAuthOption(&image) + if err != nil { + m.log.Warn("unable detect auth, continue unauthenticated", "error", err) + } + opts = append(opts, crane.WithContext(ctx)) + + tags, err := crane.ListTags(image.Destination, opts...) + if err != nil { + m.log.Error("unable to list tags of", "image", image.Source, "error", err) + errs = append(errs, err) + continue + } + + for _, tag := range tags { + // never purge latest + if tag == "latest" { + continue + } + dst := image.Destination + ":" + tag + + if slices.Contains(image.Purge.Tags, tag) { + tagsToPurge = append(tagsToPurge, dst) + } + + if image.Purge.Semver != nil { + ok, err := m.tagMatches(image.Destination, tag, *image.Purge.Semver) + if err != nil { + errs = append(errs, err) + continue + } + if ok { + tagsToPurge = append(tagsToPurge, dst) + } + } + + if !image.Purge.NoMatch { + continue + } + + tagsToCopy, err := m.getTagsToCopy(image, opts) + if err != nil { + errs = append(errs, err) + continue + } + if !slices.Contains(tagsToCopy.destinationTags(), dst) { + tagsToPurge = append(tagsToPurge, dst) + } + + } + + err = m.purge(image.Destination, tagsToPurge, opts) + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +func (m *mirror) PurgeUnknown(ctx context.Context) error { + var ( + existing []string + allowed []string + purgable []string + ) + // FIXME crane opts + registries, err := m.affectedRegistries(destinationRegistry) + if err != nil { + return err + } + + for _, registry := range registries { + catalog, err := crane.Catalog(registry) + if err != nil { + return err + } + for _, c := range catalog { + image := fmt.Sprintf("%s/%s", registry, c) + + tags, err := crane.ListTags(image) + if err != nil { + return err + } + for _, tag := range tags { + tag := tag + // never purge latest + if tag == "latest" { + continue + } + existing = append(existing, image+":"+tag) + } + } + } + for _, image := range m.config.Images { + image := image + var ( + err error + opts []crane.Option + ) + opts, err = m.ensureAuthOption(&image) + if err != nil { + m.log.Warn("unable detect auth, continue unauthenticated", "error", err) + } + opts = append(opts, crane.WithContext(ctx)) + // FIXME howto handle match.Alltags + + tagsToCopy, err := m.getTagsToCopy(image, opts) + if err != nil { + return fmt.Errorf("unable to get tags to copy:%w", err) + } + allowed = append(allowed, tagsToCopy.destinationTags()...) + } + + for _, image := range existing { + if !slices.Contains(allowed, image) { + purgable = append(purgable, image) + } + } + + for _, tag := range purgable { + m.log.Info("purge unknown", "images", tag) + // tag is the whole image refspec, split away the tag to get the image alone + lastInd := strings.LastIndex(tag, ":") + image := tag[:lastInd] + err := m.purge(image, []string{tag}, nil) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/container/purge_test.go b/pkg/container/purge_test.go new file mode 100644 index 0000000..580270d --- /dev/null +++ b/pkg/container/purge_test.go @@ -0,0 +1,157 @@ +package container_test + +import ( + "context" + "fmt" + "log/slog" + "testing" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/metal-stack/metal-lib/pkg/pointer" + apiv1 "github.com/metal-stack/oci-mirror/api/v1" + "github.com/metal-stack/oci-mirror/pkg/container" + "github.com/stretchr/testify/require" +) + +func TestPurge(t *testing.T) { + + env := map[string]string{ + "REGISTRY_STORAGE_DELETE_ENABLED": "true", + } + + dstip, dstport, err := startRegistry(env, nil, nil) + require.NoError(t, err) + dstRegistry := fmt.Sprintf("%s:%d", dstip, dstport) + + dstAlpine := fmt.Sprintf("%s/library/alpine", dstRegistry) + dstBusybox := fmt.Sprintf("%s/library/busybox", dstRegistry) + dstFoo := fmt.Sprintf("%s/library/foo", dstRegistry) + + for _, tag := range []string{"foo", "bar", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.16", "3.17", "3.18", "3.19"} { + err = createImage(dstAlpine, tag) + require.NoError(t, err) + } + for _, tag := range []string{"foo", "bar", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"} { + err = createImage(dstBusybox, tag) + require.NoError(t, err) + } + for _, tag := range []string{"foo", "bar"} { + err = createImage(dstFoo, tag) + require.NoError(t, err) + } + + config := apiv1.Config{ + Images: []apiv1.ImageMirror{ + { + Source: dstAlpine, + Destination: "http://" + dstAlpine, + Match: apiv1.Match{ + Semver: pointer.Pointer(">= 3.17"), + }, + Purge: &apiv1.Purge{ + Tags: []string{"foo"}, + Semver: pointer.Pointer("<= 3.15"), + }, + }, + { + Source: dstBusybox, + Destination: "http://" + dstBusybox, + Match: apiv1.Match{ + Semver: pointer.Pointer(">= 1.3"), + }, + Purge: &apiv1.Purge{ + NoMatch: true, + }, + }, + }, + } + + m := container.New(slog.Default(), config) + err = m.Purge(context.Background()) + require.NoError(t, err) + + tags, err := crane.ListTags(dstAlpine) + require.NoError(t, err) + require.ElementsMatch(t, []string{"bar", "3.16", "3.17", "3.18", "3.19", "latest"}, tags) + + t.Logf("alpine tags:%s", tags) + + tags, err = crane.ListTags(dstBusybox) + t.Logf("busybox tags:%s", tags) + require.NoError(t, err) + require.ElementsMatch(t, []string{"1.3", "1.4", "1.5", "1.6", "latest"}, tags) +} + +func TestPurgeUnknown(t *testing.T) { + + env := map[string]string{ + "REGISTRY_STORAGE_DELETE_ENABLED": "true", + } + + dstip, dstport, err := startRegistry(env, nil, nil) + require.NoError(t, err) + dstRegistry := fmt.Sprintf("%s:%d", dstip, dstport) + + dstAlpine := fmt.Sprintf("%s/library/alpine", dstRegistry) + dstBusybox := fmt.Sprintf("%s/library/busybox", dstRegistry) + dstFoo := fmt.Sprintf("%s/library/foo", dstRegistry) + + for _, tag := range []string{"foo", "bar", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.16", "3.17", "3.18", "3.19"} { + err = createImage(dstAlpine, tag) + require.NoError(t, err) + } + for _, tag := range []string{"foo", "bar", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"} { + err = createImage(dstBusybox, tag) + require.NoError(t, err) + } + for _, tag := range []string{"foo", "bar"} { + err = createImage(dstFoo, tag) + require.NoError(t, err) + } + + config := apiv1.Config{ + Images: []apiv1.ImageMirror{ + { + Source: dstAlpine, + Destination: "http://" + dstAlpine, + Match: apiv1.Match{ + Semver: pointer.Pointer(">= 3.17"), + }, + Purge: &apiv1.Purge{ + Tags: []string{"foo"}, + Semver: pointer.Pointer("<= 3.15"), + }, + }, + { + Source: dstBusybox, + Destination: "http://" + dstBusybox, + Match: apiv1.Match{ + Semver: pointer.Pointer(">= 1.3"), + }, + Purge: &apiv1.Purge{ + NoMatch: true, + }, + }, + }, + } + + m := container.New(slog.Default(), config) + err = m.PurgeUnknown(context.Background()) + require.NoError(t, err) + + tags, err := crane.ListTags(dstAlpine) + require.NoError(t, err) + require.ElementsMatch(t, []string{"3.17", "3.18", "3.19", "latest"}, tags) + + t.Logf("alpine tags:%s", tags) + + tags, err = crane.ListTags(dstBusybox) + t.Logf("busybox tags:%s", tags) + require.NoError(t, err) + require.ElementsMatch(t, []string{"1.3", "1.4", "1.5", "1.6", "latest"}, tags) + + tags, err = crane.ListTags(dstFoo) + t.Logf("foo tags:%s", tags) + require.NoError(t, err) + require.Empty(t, tags) +} diff --git a/pkg/container/registry.go b/pkg/container/registry.go new file mode 100644 index 0000000..a83fd61 --- /dev/null +++ b/pkg/container/registry.go @@ -0,0 +1,36 @@ +package container + +import "net/url" + +// registryTarget defines if the Registry is a source or destination registry +type registryTarget string + +const ( + // sourceRegistry is a registry where images are pulled from + sourceRegistry = registryTarget("source") + // destinationRegistry is a registry where images are pushed to + destinationRegistry = registryTarget("destination") +) + +// affectedRegistries returns a slice of all registries of sources and destinations +func (m *mirror) affectedRegistries(target registryTarget) ([]string, error) { + var ( + result []string + registries = make(map[string]bool) + ) + for _, image := range m.config.Images { + registry := image.Destination + if target == sourceRegistry { + registry = image.Source + } + parsed, err := url.Parse(registry) + if err != nil { + return nil, err + } + registries[parsed.Host] = true + } + for registry := range registries { + result = append(result, registry) + } + return result, nil +} diff --git a/pkg/container/tags.go b/pkg/container/tags.go new file mode 100644 index 0000000..5d6b120 --- /dev/null +++ b/pkg/container/tags.go @@ -0,0 +1,125 @@ +package container + +import ( + "errors" + "fmt" + "slices" + "sort" + + "github.com/Masterminds/semver/v3" + "github.com/google/go-containerregistry/pkg/crane" + apiv1 "github.com/metal-stack/oci-mirror/api/v1" +) + +type tagsToCopy map[string]string + +func (t tagsToCopy) destinationTags() []string { + var dsts []string + for _, dst := range t { + dsts = append(dsts, dst) + } + return dsts +} + +func (m *mirror) tagMatches(source, tag, semverstring string) (bool, error) { + c, err := semver.NewConstraint(semverstring) + if err != nil { + m.log.Error("unable to parse image match pattern", "error", err) + return false, err + } + v, err := semver.NewVersion(tag) + if err != nil { + m.log.Debug("pattern given, ignoring non-semver", "image", source, "tag", tag) + // This is not treated as an error + return false, nil // nolint:nilerr + } + if c.Check(v) { + return true, nil + } + return false, nil +} + +func (m *mirror) getTagsToCopy(image apiv1.ImageMirror, opts []crane.Option) (tagsToCopy, error) { + var ( + errs []error + tagsToCopy = tagsToCopy{} + semverTags []*semver.Version + ) + + tags, err := crane.ListTags(image.Source, opts...) + if err != nil { + m.log.Error("unable to list tags of", "image", image.Source, "error", err) + return nil, fmt.Errorf("unable to list tags of image:%q error %w", image.Source, err) + } + + for _, tag := range tags { + src := image.Source + ":" + tag + dst := image.Destination + ":" + tag + + if slices.Contains(image.Match.Tags, tag) { + tagsToCopy[src] = dst + } + + if image.Match.Semver != nil { + ok, err := m.tagMatches(image.Source, tag, *image.Match.Semver) + if err != nil { + errs = append(errs, err) + continue + } + if ok { + tagsToCopy[src] = dst + } + } + + if image.Match.Last != nil && *image.Match.Last > 0 { + v, err := semver.NewVersion(tag) + if err != nil { + continue + } + semverTags = append(semverTags, v) + } + } + + // If only the last n images + sort.Sort(semver.Collection(semverTags)) + + if image.Match.Last != nil && semverTags != nil { + for _, v := range semverTags[len(semverTags)-int(*image.Match.Last):] { + if slices.Contains(tags, v.String()) { + src := image.Source + ":" + v.String() + dst := image.Destination + ":" + v.String() + tagsToCopy[src] = dst + } + } + } + + if len(errs) > 0 { + return tagsToCopy, errors.Join(errs...) + } + return tagsToCopy, nil +} + +func (m *mirror) purge(image string, tags []string, opts []crane.Option) error { + var errs []error + for _, tag := range tags { + tag := tag + digest, err := crane.Digest(tag, opts...) + if err != nil { + errs = append(errs, fmt.Errorf("unable to get digest for %q %w", tag, err)) + continue + } + + dst := image + "@" + digest + m.log.Info("purge image", "tag", tag, "dst", dst) + err = crane.Delete(dst, opts...) + if err != nil { + errs = append(errs, fmt.Errorf("unable to delete digest %q %w", dst, err)) + continue + } + m.log.Info("purged image", "tag", tag, "dst", dst) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go deleted file mode 100644 index fee0b83..0000000 --- a/pkg/mirror/mirror.go +++ /dev/null @@ -1,189 +0,0 @@ -package mirror - -import ( - "context" - "encoding/json" - "errors" - "log/slog" - "slices" - "sort" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/Masterminds/semver/v3" - apiv1 "github.com/metal-stack/oci-mirror/api/v1" -) - -type mirror struct { - log *slog.Logger - config apiv1.Config -} - -func New(log *slog.Logger, config apiv1.Config) *mirror { - return &mirror{ - log: log, - config: config, - } -} - -func (m *mirror) Mirror(ctx context.Context) error { - var ( - errs []error - ) - for _, image := range m.config.Images { - var ( - err error - opts []crane.Option - ) - if strings.HasPrefix(image.Destination, "http://") { - opts = append(opts, crane.Insecure) - image.Destination = strings.ReplaceAll(image.Destination, "http://", "") - } - - auth, err := m.getAuthOption(image) - if err != nil { - m.log.Warn("unable detect auth, continue unauthenticated", "error", err) - } - if auth != nil { - opts = append(opts, auth) - } - - m.log.Info("consider mirror from", "source", image.Source, "destination", image.Destination) - - if _, err := name.ParseReference(image.Source); err != nil { - m.log.Error("given image source is malformed", "image", image.Source, "error", err) - errs = append(errs, err) - continue - } - - if _, err := name.ParseReference(image.Destination); err != nil { - m.log.Error("given image destination is malformed", "image", image.Destination, "error", err) - errs = append(errs, err) - continue - } - - if image.Match.AllTags { - m.log.Info("mirror all tags from", "source", image.Source, "destination", image.Destination) - err := crane.CopyRepository(image.Source, image.Destination, opts...) - if err != nil { - m.log.Error("unable to copy all images", "image", image.Source, "error", err) - errs = append(errs, err) - } - continue - } - - tags, err := crane.ListTags(image.Source) - if err != nil { - m.log.Error("unable to list tags of", "image", image.Source, "error", err) - errs = append(errs, err) - continue - } - - var ( - tagsToCopy = make(map[string]string) - semverTags []*semver.Version - ) - - for _, tag := range tags { - src := image.Source + ":" + tag - dst := image.Destination + ":" + tag - - if slices.Contains(image.Match.Tags, tag) { - tagsToCopy[src] = dst - } - - if image.Match.Semver != nil { - c, err := semver.NewConstraint(*image.Match.Semver) - if err != nil { - m.log.Error("unable to parse image match pattern", "error", err) - errs = append(errs, err) - continue - } - v, err := semver.NewVersion(tag) - if err != nil { - m.log.Debug("pattern given, ignoring non-semver", "image", image.Source, "tag", tag) - // This is not treated as an error - continue - } - if c.Check(v) { - tagsToCopy[src] = dst - } - } - - if image.Match.Last != nil && *image.Match.Last > 0 { - v, err := semver.NewVersion(tag) - if err != nil { - continue - } - semverTags = append(semverTags, v) - } - } - - // If only the last n images - sort.Sort(semver.Collection(semverTags)) - - if image.Match.Last != nil && semverTags != nil { - for _, v := range semverTags[len(semverTags)-int(*image.Match.Last):] { - if slices.Contains(tags, v.String()) { - src := image.Source + ":" + v.String() - dst := image.Destination + ":" + v.String() - tagsToCopy[src] = dst - } - } - } - - for src, dst := range tagsToCopy { - if !strings.HasSuffix(dst, ":latest") { - opts = append(opts, crane.WithNoClobber(false)) - } - m.log.Info("mirror from", "source", src, "destination", dst) - rawmanifest, err := crane.Manifest(src, opts...) - if err != nil { - m.log.Error("unable to read image manifest", "error", err) - errs = append(errs, err) - continue - } - manifest := v1.Manifest{} - if err := json.Unmarshal(rawmanifest, &manifest); err != nil { - m.log.Error("unable to decode image manifest", "error", err) - errs = append(errs, err) - continue - } - if manifest.SchemaVersion < 2 { - m.log.Warn("image manifest scheme version to low, ignoring", "image", src, "scheme version", manifest.SchemaVersion) - continue - } - err = crane.Copy(src, dst, opts...) - if err != nil { - m.log.Error("unable to copy", "source", src, "dst", dst, "error", err) - errs = append(errs, err) - } - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - return nil -} - -func (m *mirror) getAuthOption(image apiv1.ImageMirror) (crane.Option, error) { - dstRef, err := name.ParseReference(image.Destination) - if err != nil { - return nil, err - } - registryName := dstRef.Context().Registry.Name() - registry, ok := m.config.Registries[registryName] - if !ok { - return nil, nil - } - auth := crane.WithAuth(&authn.Basic{ - Username: registry.Auth.Username, - Password: registry.Auth.Password, - }) - return auth, nil -}