Skip to content

Commit c6f6813

Browse files
committed
feat: delete unused images after a configurable amount of time
1 parent 01c7696 commit c6f6813

File tree

9 files changed

+61
-8
lines changed

9 files changed

+61
-8
lines changed

api/kuik/v1alpha1/image_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ type ImageStatus struct {
7575
UsedByPods ReferencesWithCount `json:"usedByPods,omitempty"`
7676
// Upstream is the information about the upstream image
7777
Upstream Upstream `json:"upstream,omitempty"`
78+
// UnusedSince is the time when the image was last used by a pod
79+
UnusedSince metav1.Time `json:"unusedSince,omitempty"`
7880
}
7981

8082
// +kubebuilder:object:root=true
@@ -85,6 +87,7 @@ type ImageStatus struct {
8587
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image"
8688
// +kubebuilder:printcolumn:name="Pods count",type="integer",JSONPath=".status.usedByPods.count"
8789
// +kubebuilder:printcolumn:name="Upstream status",type="string",JSONPath=".status.upstream.status"
90+
// +kubebuilder:printcolumn:name="Unused since",type="date",JSONPath=".status.unusedSince"
8891
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
8992

9093
// Image is the Schema for the images API.

api/kuik/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/main.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"flag"
66
"os"
77
"path/filepath"
8+
"time"
89

910
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
1011
// to ensure that exec-entrypoint and run can make use of them.
@@ -51,6 +52,7 @@ func main() {
5152
var probeAddr string
5253
var secureMetrics bool
5354
var enableHTTP2 bool
55+
var unusedImageTTL int
5456
var tlsOpts []func(*tls.Config)
5557
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
5658
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -69,6 +71,8 @@ func main() {
6971
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
7072
flag.BoolVar(&enableHTTP2, "enable-http2", false,
7173
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
74+
flag.IntVar(&unusedImageTTL, "unused-image-ttl", 30, "Unused image TTL in days.")
75+
7276
opts := zap.Options{
7377
Development: true,
7478
}
@@ -198,8 +202,9 @@ func main() {
198202
os.Exit(1)
199203
}
200204
if err = (&kuikcontroller.ImageReconciler{
201-
Client: mgr.GetClient(),
202-
Scheme: mgr.GetScheme(),
205+
Client: mgr.GetClient(),
206+
Scheme: mgr.GetScheme(),
207+
UnusedImageTTL: time.Hour * 24 * time.Duration(unusedImageTTL),
203208
}).SetupWithManager(mgr); err != nil {
204209
setupLog.Error(err, "unable to create controller", "controller", "Image")
205210
os.Exit(1)

config/crd/bases/kuik.enix.io_images.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ spec:
2929
- jsonPath: .status.upstream.status
3030
name: Upstream status
3131
type: string
32+
- jsonPath: .status.unusedSince
33+
name: Unused since
34+
type: date
3235
- jsonPath: .metadata.creationTimestamp
3336
name: Age
3437
type: date
@@ -70,6 +73,11 @@ spec:
7073
status:
7174
description: ImageStatus defines the observed state of Image.
7275
properties:
76+
unusedSince:
77+
description: UnusedSince is the time when the image was last used
78+
by a pod
79+
format: date-time
80+
type: string
7381
upstream:
7482
description: Upstream is the information about the upstream image
7583
properties:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/onsi/ginkgo/v2 v2.23.3
1515
github.com/onsi/gomega v1.36.3
1616
github.com/prometheus/client_golang v1.22.0
17+
go.uber.org/zap v1.27.0
1718
k8s.io/api v0.32.3
1819
k8s.io/apimachinery v0.32.3
1920
k8s.io/client-go v1.5.2
@@ -88,7 +89,6 @@ require (
8889
go.opentelemetry.io/otel/trace v1.35.0 // indirect
8990
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
9091
go.uber.org/multierr v1.11.0 // indirect
91-
go.uber.org/zap v1.27.0 // indirect
9292
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
9393
golang.org/x/net v0.39.0 // indirect
9494
golang.org/x/oauth2 v0.29.0 // indirect

helm/kube-image-keeper/crds/kuik.enix.io_images.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ spec:
2828
- jsonPath: .status.upstream.status
2929
name: Upstream status
3030
type: string
31+
- jsonPath: .status.unusedSince
32+
name: Unused since
33+
type: date
3134
- jsonPath: .metadata.creationTimestamp
3235
name: Age
3336
type: date
@@ -69,6 +72,11 @@ spec:
6972
status:
7073
description: ImageStatus defines the observed state of Image.
7174
properties:
75+
unusedSince:
76+
description: UnusedSince is the time when the image was last used
77+
by a pod
78+
format: date-time
79+
type: string
7280
upstream:
7381
description: Upstream is the information about the upstream image
7482
properties:

helm/kube-image-keeper/templates/manager-deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ spec:
4242
- -metrics-bind-address=:8080
4343
- -metrics-secure=false
4444
- -zap-log-level={{ .Values.manager.verbosity }}
45+
- -unused-image-ttl={{ .Values.unusedImageTTL }}
4546
env:
4647
- name: KUIK_REGISTRY_MONITOR_DEFAULT_INTERVAL
4748
value: "{{ .Values.registryMonitors.defaultSpec.interval }}"

helm/kube-image-keeper/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,6 @@ registryMonitors:
104104
ghcr.io: {}
105105
registry.k8s.io: {}
106106
public.ecr.aws: {}
107+
108+
# -- Delay in days before deleting an Image not used by any pod
109+
unusedImageTTL: 30

internal/controller/kuik/image_controller.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package kuik
33
import (
44
"context"
55
"net/http"
6+
"time"
67

78
kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1"
89
corev1 "k8s.io/api/core/v1"
910
"k8s.io/apimachinery/pkg/api/errors"
1011
apierrors "k8s.io/apimachinery/pkg/api/errors"
12+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1113
"k8s.io/apimachinery/pkg/runtime"
1214
"k8s.io/apimachinery/pkg/types"
1315
ctrl "sigs.k8s.io/controller-runtime"
@@ -23,7 +25,8 @@ const (
2325
// ImageReconciler reconciles a Image object
2426
type ImageReconciler struct {
2527
client.Client
26-
Scheme *runtime.Scheme
28+
Scheme *runtime.Scheme
29+
UnusedImageTTL time.Duration
2730
}
2831

2932
// +kubebuilder:rbac:groups=kuik.enix.io,resources=images,verbs=get;list;watch;create;update;patch;delete
@@ -40,20 +43,33 @@ type ImageReconciler struct {
4043
// For more details, check Reconcile and its Result here:
4144
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
4245
func (r *ImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
43-
_ = logf.FromContext(ctx)
46+
log := logf.FromContext(ctx)
4447

4548
var image kuikv1alpha1.Image
4649
if err := r.Get(ctx, req.NamespacedName, &image); err != nil {
4750
return ctrl.Result{}, client.IgnoreNotFound(err)
4851
}
4952

53+
log = log.WithValues("reference", image.Reference())
54+
5055
// update Pods and Nodes status for this Image
5156
if requeue, err := r.updateReferenceCount(ctx, &image); requeue {
5257
return ctrl.Result{Requeue: true}, nil
5358
} else if err != nil {
5459
return ctrl.Result{}, err
5560
}
5661

62+
if !image.Status.UnusedSince.IsZero() {
63+
if v1.Now().Sub(image.Status.UnusedSince.Time) > r.UnusedImageTTL {
64+
log.Info("image is unused for too long, deleting it", "ttl", r.UnusedImageTTL)
65+
if err := r.Delete(ctx, &image); err != nil {
66+
return ctrl.Result{}, client.IgnoreNotFound(err)
67+
}
68+
} else {
69+
return ctrl.Result{RequeueAfter: r.UnusedImageTTL - time.Since(image.Status.UnusedSince.Time)}, nil
70+
}
71+
}
72+
5773
return ctrl.Result{}, nil
5874
}
5975

@@ -88,7 +104,7 @@ func (r *ImageReconciler) SetupWithManager(mgr ctrl.Manager) error {
88104
func (r *ImageReconciler) updateReferenceCount(ctx context.Context, image *kuikv1alpha1.Image) (requeue bool, err error) {
89105
var podList corev1.PodList
90106
if err = r.List(ctx, &podList, client.MatchingFields{ImagesIndexKey: image.Name}); err != nil && !apierrors.IsNotFound(err) {
91-
return
107+
return false, err
92108
}
93109

94110
pods := []string{}
@@ -104,15 +120,23 @@ func (r *ImageReconciler) updateReferenceCount(ctx context.Context, image *kuikv
104120
Count: len(pods),
105121
}
106122

123+
if len(pods) == 0 {
124+
if image.Status.UnusedSince.IsZero() {
125+
image.Status.UnusedSince = v1.Now()
126+
}
127+
} else {
128+
image.Status.UnusedSince = v1.Time{}
129+
}
130+
107131
err = r.Status().Update(ctx, image)
108132
if err != nil {
109133
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.Status().Code == http.StatusConflict {
110134
requeue = true
111135
}
112-
return
136+
return requeue, err
113137
}
114138

115-
return
139+
return false, nil
116140
}
117141

118142
func (r *ImageReconciler) imagesRequestFromPod(ctx context.Context, obj client.Object) []ctrl.Request {

0 commit comments

Comments
 (0)