Skip to content

Commit e6c127a

Browse files
added additional tests for new functionality
1 parent aa7247e commit e6c127a

3 files changed

Lines changed: 510 additions & 41 deletions

File tree

controllers/managedcloudprofile_controller.go

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ const (
3131
CloudProfileAppliedConditionType string = "CloudProfileApplied"
3232
)
3333

34+
// OCIFactory provides a hook for constructing OCI sources. It defaults to
35+
// cloudprofilesync.NewOCI but can be overridden in tests to simulate errors.
36+
var OCIFactory = func(params cloudprofilesync.OCIParams, insecure bool) (cloudprofilesync.Source, error) {
37+
return cloudprofilesync.NewOCI(params, insecure)
38+
}
39+
3440
type Reconciler struct {
3541
client.Client
3642
}
@@ -53,6 +59,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
5359
cloudProfile.Spec = CloudProfileSpecToGardener(&mcp.Spec.CloudProfile)
5460
errs := make([]error, 0)
5561
for _, updates := range mcp.Spec.MachineImageUpdates {
62+
// only call updater when an explicit source is provided
63+
if updates.Source.OCI == nil {
64+
continue
65+
}
5666
errs = append(errs, r.updateMachineImages(ctx, log, updates, &cloudProfile.Spec))
5767
}
5868
gardenerv1beta1.SetObjectDefaults_CloudProfile(&cloudProfile)
@@ -96,31 +106,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
96106
case updates.Source.OCI != nil:
97107
password, err := r.getCredential(ctx, updates.Source.OCI.Password)
98108
if err != nil {
109+
// credential errors already patched by getCredential caller
99110
return ctrl.Result{}, err
100111
}
101-
oci, err := cloudprofilesync.NewOCI(cloudprofilesync.OCIParams{
112+
src, err := OCIFactory(cloudprofilesync.OCIParams{
102113
Registry: updates.Source.OCI.Registry,
103114
Repository: updates.Source.OCI.Repository,
104115
Username: updates.Source.OCI.Username,
105116
Password: string(password),
106117
Parallel: 1,
107118
}, updates.Source.OCI.Insecure)
108119
if err != nil {
120+
// patch status before returning so the failure is visible on the MCP
121+
_ = r.patchStatusAndCondition(ctx, &mcp, v1alpha1.FailedReconcileStatus, metav1.Condition{
122+
Type: CloudProfileAppliedConditionType,
123+
Status: metav1.ConditionFalse,
124+
ObservedGeneration: mcp.Generation,
125+
Reason: "GarbageCollectionFailed",
126+
Message: fmt.Sprintf("failed to initialize OCI source for garbage collection: %s", err),
127+
})
109128
return ctrl.Result{}, fmt.Errorf("failed to initialize OCI source for garbage collection: %w", err)
110129
}
111-
source = oci
130+
source = src
112131
default:
113132
continue
114133
}
115134

116135
versions, err := source.GetVersions(ctx)
117136
if err != nil {
137+
_ = r.patchStatusAndCondition(ctx, &mcp, v1alpha1.FailedReconcileStatus, metav1.Condition{
138+
Type: CloudProfileAppliedConditionType,
139+
Status: metav1.ConditionFalse,
140+
ObservedGeneration: mcp.Generation,
141+
Reason: "GarbageCollectionFailed",
142+
Message: fmt.Sprintf("failed to list source versions for garbage collection: %s", err),
143+
})
118144
return ctrl.Result{}, fmt.Errorf("failed to list source versions for garbage collection: %w", err)
119145
}
120146

121147
referencedVersions := r.getReferencedVersions(ctx, mcp.Name, updates.ImageName)
122148

123149
cutoff := time.Now().Add(-updates.GarbageCollection.MaxAge.Duration)
150+
versionsToDelete := make([]string, 0)
124151
for _, v := range versions {
125152
if v.CreatedAt.IsZero() {
126153
continue
@@ -129,36 +156,55 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
129156
continue
130157
}
131158
if v.CreatedAt.Before(cutoff) {
132-
if err := r.deleteVersion(ctx, mcp.Name, updates.ImageName, v.Version); err != nil {
133-
if apierrors.IsInvalid(err) {
134-
log.V(1).Info("garbage collection validation failed, skipping", "image", updates.ImageName, "version", v.Version)
135-
continue
136-
}
137-
return ctrl.Result{}, fmt.Errorf("failed to delete image version: %w", err)
159+
versionsToDelete = append(versionsToDelete, v.Version)
160+
}
161+
}
162+
163+
if len(versionsToDelete) > 0 {
164+
if err := r.deleteVersion(ctx, mcp.Name, updates.ImageName, versionsToDelete); err != nil {
165+
if apierrors.IsInvalid(err) {
166+
log.V(1).Info("garbage collection validation failed, skipping", "image", updates.ImageName)
167+
continue
138168
}
139-
log.Info("deleted image version from CloudProfile", "image", updates.ImageName, "version", v.Version)
169+
// patch status before returning so caller sees the failure
170+
_ = r.patchStatusAndCondition(ctx, &mcp, v1alpha1.FailedReconcileStatus, metav1.Condition{
171+
Type: CloudProfileAppliedConditionType,
172+
Status: metav1.ConditionFalse,
173+
ObservedGeneration: mcp.Generation,
174+
Reason: "GarbageCollectionFailed",
175+
Message: fmt.Sprintf("failed to delete image versions: %s", err),
176+
})
177+
return ctrl.Result{}, fmt.Errorf("failed to delete image versions: %w", err)
178+
}
179+
for _, v := range versionsToDelete {
180+
log.Info("deleted image version from CloudProfile", "image", updates.ImageName, "version", v)
140181
}
141182
}
142183
}
143184

144185
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
145186
}
146187

147-
func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageName, version string) error {
188+
func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageName string, versions []string) error {
148189
var cp gardenerv1beta1.CloudProfile
149190
if err := r.Get(ctx, types.NamespacedName{Name: cloudProfileName}, &cp); err != nil {
150191
return err
151192
}
193+
194+
versionsSet := make(map[string]bool)
195+
for _, v := range versions {
196+
versionsSet[v] = true
197+
}
198+
152199
for i := range cp.Spec.MachineImages {
153200
if cp.Spec.MachineImages[i].Name != imageName {
154201
continue
155202
}
156203
newVersions := make([]gardenerv1beta1.MachineImageVersion, 0, len(cp.Spec.MachineImages[i].Versions))
157204
for _, mv := range cp.Spec.MachineImages[i].Versions {
158-
if mv.Version == version {
159-
continue
205+
if !versionsSet[mv.Version] {
206+
newVersions = append(newVersions, mv)
160207
}
161-
newVersions = append(newVersions, mv)
162208
}
163209
cp.Spec.MachineImages[i].Versions = newVersions
164210
}
@@ -171,10 +217,16 @@ func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageN
171217
}
172218
filtered := make([]providercfg.MachineImageVersion, 0, len(cfg.MachineImages[i].Versions))
173219
for _, mv := range cfg.MachineImages[i].Versions {
174-
if strings.HasSuffix(mv.Image, ":"+version) {
175-
continue
220+
found := false
221+
for _, version := range versions {
222+
if strings.HasSuffix(mv.Image, ":"+version) {
223+
found = true
224+
break
225+
}
226+
}
227+
if !found {
228+
filtered = append(filtered, mv)
176229
}
177-
filtered = append(filtered, mv)
178230
}
179231
cfg.MachineImages[i].Versions = filtered
180232
}
@@ -191,22 +243,40 @@ func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageN
191243
func (r *Reconciler) getReferencedVersions(ctx context.Context, cloudProfileName, imageName string) map[string]bool {
192244
referenced := make(map[string]bool)
193245

194-
shootList := &gardenerv1beta1.ShootList{}
195-
if err := r.List(ctx, shootList, client.InNamespace(metav1.NamespaceAll)); err != nil {
196-
return referenced
197-
}
198-
199-
for _, shoot := range shootList.Items {
200-
if shoot.Spec.CloudProfile == nil || shoot.Spec.CloudProfile.Name != cloudProfileName {
201-
continue
246+
var cp gardenerv1beta1.CloudProfile
247+
if err := r.Get(ctx, types.NamespacedName{Name: cloudProfileName}, &cp); err == nil {
248+
if cp.Spec.ProviderConfig != nil {
249+
var cfg providercfg.CloudProfileConfig
250+
if err := json.Unmarshal(cp.Spec.ProviderConfig.Raw, &cfg); err == nil {
251+
for _, img := range cfg.MachineImages {
252+
if img.Name != imageName {
253+
continue
254+
}
255+
for _, v := range img.Versions {
256+
if idx := strings.LastIndex(v.Image, ":"); idx != -1 {
257+
version := v.Image[idx+1:]
258+
referenced[version] = true
259+
}
260+
}
261+
}
262+
}
202263
}
264+
}
203265

204-
for _, worker := range shoot.Spec.Provider.Workers {
205-
if worker.Machine.Image == nil || worker.Machine.Image.Name != imageName {
266+
shootList := &gardenerv1beta1.ShootList{}
267+
if err := r.List(ctx, shootList, client.InNamespace(metav1.NamespaceAll)); err == nil {
268+
for _, shoot := range shootList.Items {
269+
if shoot.Spec.CloudProfile == nil || shoot.Spec.CloudProfile.Name != cloudProfileName {
206270
continue
207271
}
208-
if worker.Machine.Image.Version != nil {
209-
referenced[*worker.Machine.Image.Version] = true
272+
273+
for _, worker := range shoot.Spec.Provider.Workers {
274+
if worker.Machine.Image == nil || worker.Machine.Image.Name != imageName {
275+
continue
276+
}
277+
if worker.Machine.Image.Version != nil {
278+
referenced[*worker.Machine.Image.Version] = true
279+
}
210280
}
211281
}
212282
}
@@ -222,7 +292,7 @@ func (r *Reconciler) updateMachineImages(ctx context.Context, log logr.Logger, u
222292
if err != nil {
223293
return err
224294
}
225-
oci, err := cloudprofilesync.NewOCI(cloudprofilesync.OCIParams{
295+
src, err := OCIFactory(cloudprofilesync.OCIParams{
226296
Registry: update.Source.OCI.Registry,
227297
Repository: update.Source.OCI.Repository,
228298
Username: update.Source.OCI.Username,
@@ -232,7 +302,7 @@ func (r *Reconciler) updateMachineImages(ctx context.Context, log logr.Logger, u
232302
if err != nil {
233303
return fmt.Errorf("failed to initialize oci source: %w", err)
234304
}
235-
source = oci
305+
source = src
236306
default:
237307
return errors.New("no machine images source configured")
238308
}

0 commit comments

Comments
 (0)