Skip to content

Commit 23ce61d

Browse files
committed
feat(controllers): add a finalizer on Repositories
1 parent 3a0cc36 commit 23ce61d

File tree

6 files changed

+349
-27
lines changed

6 files changed

+349
-27
lines changed

api/v1alpha1/repository_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ type RepositorySpec struct {
1313

1414
// RepositoryStatus defines the observed state of Repository
1515
type RepositoryStatus struct {
16+
Phase string `json:"phase,omitempty"`
17+
//+listType=map
18+
//+listMapKey=type
19+
//+patchStrategy=merge
20+
//+patchMergeKey=type
21+
//+optional
22+
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
1623
}
1724

1825
//+kubebuilder:object:root=true
1926
//+kubebuilder:subresource:status
2027
//+kubebuilder:resource:scope=Cluster,shortName=repo
28+
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase"
29+
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
2130

2231
// Repository is the Schema for the repositories API
2332
type Repository struct {

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ spec:
1717
singular: repository
1818
scope: Cluster
1919
versions:
20-
- name: v1alpha1
20+
- additionalPrinterColumns:
21+
- jsonPath: .status.phase
22+
name: Status
23+
type: string
24+
- jsonPath: .metadata.creationTimestamp
25+
name: Age
26+
type: date
27+
name: v1alpha1
2128
schema:
2229
openAPIV3Schema:
2330
description: Repository is the Schema for the repositories API
@@ -50,6 +57,80 @@ spec:
5057
type: object
5158
status:
5259
description: RepositoryStatus defines the observed state of Repository
60+
properties:
61+
conditions:
62+
items:
63+
description: "Condition contains details for one aspect of the current
64+
state of this API Resource. --- This struct is intended for direct
65+
use as an array at the field path .status.conditions. For example,
66+
\n type FooStatus struct{ // Represents the observations of a
67+
foo's current state. // Known .status.conditions.type are: \"Available\",
68+
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
69+
// +listType=map // +listMapKey=type Conditions []metav1.Condition
70+
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
71+
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
72+
properties:
73+
lastTransitionTime:
74+
description: lastTransitionTime is the last time the condition
75+
transitioned from one status to another. This should be when
76+
the underlying condition changed. If that is not known, then
77+
using the time when the API field changed is acceptable.
78+
format: date-time
79+
type: string
80+
message:
81+
description: message is a human readable message indicating
82+
details about the transition. This may be an empty string.
83+
maxLength: 32768
84+
type: string
85+
observedGeneration:
86+
description: observedGeneration represents the .metadata.generation
87+
that the condition was set based upon. For instance, if .metadata.generation
88+
is currently 12, but the .status.conditions[x].observedGeneration
89+
is 9, the condition is out of date with respect to the current
90+
state of the instance.
91+
format: int64
92+
minimum: 0
93+
type: integer
94+
reason:
95+
description: reason contains a programmatic identifier indicating
96+
the reason for the condition's last transition. Producers
97+
of specific condition types may define expected values and
98+
meanings for this field, and whether the values are considered
99+
a guaranteed API. The value should be a CamelCase string.
100+
This field may not be empty.
101+
maxLength: 1024
102+
minLength: 1
103+
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
104+
type: string
105+
status:
106+
description: status of the condition, one of True, False, Unknown.
107+
enum:
108+
- "True"
109+
- "False"
110+
- Unknown
111+
type: string
112+
type:
113+
description: type of condition in CamelCase or in foo.example.com/CamelCase.
114+
--- Many .condition.type values are consistent across resources
115+
like Available, but because arbitrary conditions can be useful
116+
(see .node.status.conditions), the ability to deconflict is
117+
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
118+
maxLength: 316
119+
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
120+
type: string
121+
required:
122+
- lastTransitionTime
123+
- message
124+
- reason
125+
- status
126+
- type
127+
type: object
128+
type: array
129+
x-kubernetes-list-map-keys:
130+
- type
131+
x-kubernetes-list-type: map
132+
phase:
133+
type: string
53134
type: object
54135
type: object
55136
served: true

controllers/cachedimage_controller.go

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import (
3333
"github.com/enix/kube-image-keeper/internal/registry"
3434
)
3535

36-
// https://book.kubebuilder.io/reference/using-finalizers.html
37-
const cachedImageFinalizerName = "cachedimage.kuik.enix.io/finalizer"
36+
const (
37+
cachedImageFinalizerName = "cachedimage.kuik.enix.io/finalizer"
38+
repositoryOwnerKey = ".metadata.repositoryOwner"
39+
)
3840

3941
// CachedImageReconciler reconciles a CachedImage object
4042
type CachedImageReconciler struct {
@@ -113,6 +115,37 @@ func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request)
113115
return ctrl.Result{}, nil
114116
}
115117

118+
// Create or patch related repository
119+
named, err := cachedImage.Repository()
120+
if err != nil {
121+
return ctrl.Result{}, err
122+
}
123+
124+
repositoryName := named.Name()
125+
repository := kuikv1alpha1.Repository{ObjectMeta: metav1.ObjectMeta{Name: registry.SanitizeName(repositoryName)}}
126+
operation, err := controllerutil.CreateOrPatch(ctx, r.Client, &repository, func() error {
127+
repository.Spec.Name = repositoryName
128+
return nil
129+
})
130+
131+
if err != nil {
132+
return ctrl.Result{}, err
133+
}
134+
135+
log.Info("repository updated", "repository", klog.KObj(&repository), "operation", operation)
136+
137+
// Set owner reference
138+
owner := &kuikv1alpha1.Repository{}
139+
if err := r.Get(context.Background(), client.ObjectKeyFromObject(&repository), owner); err != nil {
140+
return ctrl.Result{}, err
141+
}
142+
if err := controllerutil.SetOwnerReference(owner, &cachedImage, r.Scheme); err != nil {
143+
return ctrl.Result{}, err
144+
}
145+
if err := r.Update(ctx, &cachedImage); err != nil {
146+
return ctrl.Result{}, err
147+
}
148+
116149
// Remove image from registry when CachedImage is being deleted, finalizer is removed after it
117150
if !cachedImage.ObjectMeta.DeletionTimestamp.IsZero() {
118151
if controllerutil.ContainsFinalizer(&cachedImage, cachedImageFinalizerName) {
@@ -196,24 +229,6 @@ func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request)
196229
}
197230
}
198231

199-
named, err := cachedImage.Repository()
200-
if err != nil {
201-
return ctrl.Result{}, err
202-
}
203-
204-
repositoryName := named.Name()
205-
repository := kuikv1alpha1.Repository{ObjectMeta: metav1.ObjectMeta{Name: registry.SanitizeName(repositoryName)}}
206-
operation, err := controllerutil.CreateOrPatch(ctx, r.Client, &repository, func() error {
207-
repository.Spec.Name = repositoryName
208-
return nil
209-
})
210-
211-
if err != nil {
212-
return ctrl.Result{}, err
213-
}
214-
215-
log.Info("repository reconcilied", "repository", klog.KObj(&repository), "operation", operation)
216-
217232
// Adding image to registry
218233
log.Info("caching image")
219234
isCached, err := registry.ImageIsCached(cachedImage.Spec.SourceImage)

controllers/pod_controller.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func (r *PodReconciler) podsWithDeletingCachedImages(obj client.Object) []ctrl.R
158158
cachedImage := obj.(*kuikv1alpha1.CachedImage)
159159
var currentCachedImage kuikv1alpha1.CachedImage
160160
// wait for the CachedImage to be really deleted
161-
if err := r.Get(context.Background(), types.NamespacedName{Name: cachedImage.Name}, &currentCachedImage); err == nil || !apierrors.IsNotFound(err) {
161+
if err := r.Get(context.Background(), client.ObjectKeyFromObject(cachedImage), &currentCachedImage); err == nil || !apierrors.IsNotFound(err) {
162162
return make([]ctrl.Request, 0)
163163
}
164164

@@ -179,8 +179,7 @@ func (r *PodReconciler) podsWithDeletingCachedImages(obj client.Object) []ctrl.R
179179
if cachedImage.Spec.SourceImage == value {
180180
log.Info("image in use", "pod", pod.Namespace+"/"+pod.Name)
181181
res := make([]ctrl.Request, 1)
182-
res[0].Name = pod.Name
183-
res[0].Namespace = pod.Namespace
182+
res[0].NamespacedName = client.ObjectKeyFromObject(&pod)
184183
return res
185184
}
186185
}

controllers/repository_controller.go

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,29 @@ package controllers
33
import (
44
"context"
55

6+
apierrors "k8s.io/apimachinery/pkg/api/errors"
7+
"k8s.io/apimachinery/pkg/api/meta"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
69
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/apimachinery/pkg/types"
711
ctrl "sigs.k8s.io/controller-runtime"
12+
"sigs.k8s.io/controller-runtime/pkg/builder"
813
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
15+
"sigs.k8s.io/controller-runtime/pkg/event"
16+
"sigs.k8s.io/controller-runtime/pkg/handler"
917
"sigs.k8s.io/controller-runtime/pkg/log"
18+
"sigs.k8s.io/controller-runtime/pkg/predicate"
19+
"sigs.k8s.io/controller-runtime/pkg/source"
1020

1121
kuikv1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1"
1222
)
1323

24+
const (
25+
repositoryFinalizerName = "repository.kuik.enix.io/finalizer"
26+
typeReadyRepository = "Ready"
27+
)
28+
1429
// RepositoryReconciler reconciles a Repository object
1530
type RepositoryReconciler struct {
1631
client.Client
@@ -31,16 +46,138 @@ type RepositoryReconciler struct {
3146
// For more details, check Reconcile and its Result here:
3247
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
3348
func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
34-
_ = log.FromContext(ctx)
49+
log := log.FromContext(ctx)
50+
51+
var repository kuikv1alpha1.Repository
52+
if err := r.Get(ctx, req.NamespacedName, &repository); err != nil {
53+
return ctrl.Result{}, client.IgnoreNotFound(err)
54+
}
55+
56+
log.Info("reconciling repository")
57+
58+
if !repository.ObjectMeta.DeletionTimestamp.IsZero() {
59+
r.UpdateStatus(ctx, &repository, []metav1.Condition{{
60+
Type: typeReadyRepository,
61+
Status: metav1.ConditionFalse,
62+
Reason: "AskedForDeletion",
63+
Message: "Repository has been asked to be deleted",
64+
}})
65+
66+
if controllerutil.ContainsFinalizer(&repository, repositoryFinalizerName) {
67+
var cachedImageList kuikv1alpha1.CachedImageList
68+
if err := r.List(ctx, &cachedImageList, client.MatchingFields{repositoryOwnerKey: repository.Name}); err != nil && !apierrors.IsNotFound(err) {
69+
return ctrl.Result{}, err
70+
}
71+
72+
log.Info("repository is deleting", "cachedImages", len(cachedImageList.Items))
73+
if len(cachedImageList.Items) > 0 {
74+
return ctrl.Result{}, nil
75+
}
3576

36-
// TODO(user): your logic here
77+
log.Info("removing finalizer")
78+
controllerutil.RemoveFinalizer(&repository, repositoryFinalizerName)
79+
if err := r.Update(ctx, &repository); err != nil {
80+
return ctrl.Result{}, err
81+
}
82+
}
83+
84+
return ctrl.Result{}, nil
85+
}
86+
87+
err := r.UpdateStatus(ctx, &repository, []metav1.Condition{{
88+
Type: typeReadyRepository,
89+
Status: metav1.ConditionTrue,
90+
Reason: "Created",
91+
Message: "Repository is ready",
92+
}})
93+
if err != nil {
94+
return ctrl.Result{}, err
95+
}
96+
97+
// Add finalizer to keep the Repository during image removal from registry on deletion
98+
if !controllerutil.ContainsFinalizer(&repository, repositoryFinalizerName) {
99+
log.Info("adding finalizer")
100+
controllerutil.AddFinalizer(&repository, repositoryFinalizerName)
101+
if err := r.Update(ctx, &repository); err != nil {
102+
return ctrl.Result{}, err
103+
}
104+
}
37105

38106
return ctrl.Result{}, nil
39107
}
40108

109+
func (r *RepositoryReconciler) UpdateStatus(ctx context.Context, repository *kuikv1alpha1.Repository, conditions []metav1.Condition) error {
110+
log := log.FromContext(ctx)
111+
112+
for _, condition := range conditions {
113+
meta.SetStatusCondition(&repository.Status.Conditions, condition)
114+
}
115+
116+
conditionReady := meta.FindStatusCondition(repository.Status.Conditions, typeReadyRepository)
117+
if conditionReady.Status == metav1.ConditionTrue {
118+
repository.Status.Phase = "Ready"
119+
} else if conditionReady.Status == metav1.ConditionFalse {
120+
repository.Status.Phase = "Terminating"
121+
} else {
122+
repository.Status.Phase = ""
123+
}
124+
125+
if err := r.Status().Update(ctx, repository); err != nil {
126+
log.Error(err, "Failed to update Repository status")
127+
return err
128+
}
129+
130+
return nil
131+
}
132+
41133
// SetupWithManager sets up the controller with the Manager.
42134
func (r *RepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
135+
p := predicate.Funcs{
136+
DeleteFunc: func(e event.DeleteEvent) bool {
137+
return true
138+
},
139+
}
140+
141+
// Create an index to list CachedImage by Repository
142+
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kuikv1alpha1.CachedImage{}, repositoryOwnerKey, func(rawObj client.Object) []string {
143+
cachedImage := rawObj.(*kuikv1alpha1.CachedImage)
144+
145+
owners := cachedImage.GetOwnerReferences()
146+
for _, owner := range owners {
147+
if owner.APIVersion != kuikv1alpha1.GroupVersion.String() || owner.Kind != "Repository" {
148+
return nil
149+
}
150+
151+
return []string{owner.Name}
152+
}
153+
154+
return []string{}
155+
}); err != nil {
156+
return err
157+
}
158+
43159
return ctrl.NewControllerManagedBy(mgr).
44160
For(&kuikv1alpha1.Repository{}).
161+
Watches(
162+
&source.Kind{Type: &kuikv1alpha1.CachedImage{}},
163+
handler.EnqueueRequestsFromMapFunc(r.repositoryWithDeletingCachedImages),
164+
builder.WithPredicates(p),
165+
).
45166
Complete(r)
46167
}
168+
169+
func (r *RepositoryReconciler) repositoryWithDeletingCachedImages(obj client.Object) []ctrl.Request {
170+
cachedImage := obj.(*kuikv1alpha1.CachedImage)
171+
var currentCachedImage kuikv1alpha1.CachedImage
172+
// wait for the CachedImage to be really deleted
173+
if err := r.Get(context.Background(), client.ObjectKeyFromObject(cachedImage), &currentCachedImage); err == nil || !apierrors.IsNotFound(err) {
174+
return nil
175+
}
176+
177+
repositoryName, ok := cachedImage.Labels[kuikv1alpha1.RepositoryLabelName]
178+
if !ok {
179+
return nil
180+
}
181+
182+
return []ctrl.Request{{NamespacedName: types.NamespacedName{Name: repositoryName}}}
183+
}

0 commit comments

Comments
 (0)