@@ -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+
3440type 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
191243func (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