Skip to content

Commit

Permalink
Add purge support (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Feb 21, 2024
1 parent 5412b22 commit ccfa00d
Show file tree
Hide file tree
Showing 17 changed files with 962 additions and 267 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?
26 changes: 24 additions & 2 deletions api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 30 additions & 1 deletion api/v1/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestConfig_Validate(t *testing.T) {
wantErr: true,
},
{
name: "invalid semver",
name: "invalid match semver",
Images: []ImageMirror{
{
Source: "abc",
Expand All @@ -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{
Expand Down Expand Up @@ -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) {
Expand Down
82 changes: 81 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -72,6 +150,8 @@ func main() {
Usage: "oci mirror server",
Commands: []*cli.Command{
mirrorCmd,
purgeCmd,
purgeUnknwonCmd,
},
}

Expand Down
30 changes: 27 additions & 3 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
}
62 changes: 62 additions & 0 deletions deploy/oci-mirror.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit ccfa00d

Please sign in to comment.