Skip to content

Commit aa07462

Browse files
Merge pull request #5545 from bfournie/AGENT-1412-iri-deletion-guard
AGENT-1412: Prevent deletion of InternalReleaseImage when in use
2 parents 4c02195 + 22a856f commit aa07462

8 files changed

Lines changed: 151 additions & 10 deletions

File tree

cmd/machine-config-controller/start.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func runStartCmd(_ *cobra.Command, _ []string) {
143143
ctrlctx.InformerFactory.Machineconfiguration().V1alpha1().InternalReleaseImages(),
144144
ctrlctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
145145
ctrlctx.InformerFactory.Machineconfiguration().V1().MachineConfigs(),
146+
ctrlctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
146147
ctrlctx.ClientBuilder.KubeClientOrDie("internalreleaseimage-controller"),
147148
ctrlctx.ClientBuilder.MachineConfigClientOrDie("internalreleaseimage-controller"))
148149

cmd/machine-config-operator/start.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88

9+
features "github.com/openshift/api/features"
910
"github.com/openshift/machine-config-operator/cmd/common"
1011
"github.com/openshift/machine-config-operator/internal/clients"
1112
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
@@ -66,6 +67,13 @@ func runStartCmd(_ *cobra.Command, _ []string) {
6667
klog.Fatal(fmt.Errorf("failed to connect to feature gates %w", fgErr))
6768
}
6869

70+
// Only pass IRI informer when the feature gate is enabled to avoid
71+
// watching for a CRD that may not exist
72+
var iriInformer = ctrlctx.InformerFactory.Machineconfiguration().V1alpha1().InternalReleaseImages()
73+
if !ctrlctx.FeatureGatesHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
74+
iriInformer = nil
75+
}
76+
6977
controller := operator.New(
7078
ctrlcommon.MCONamespace, componentName,
7179
startOpts.imagesFile,
@@ -111,6 +119,7 @@ func runStartCmd(_ *cobra.Command, _ []string) {
111119
ctrlctx.NamespacedInformerFactory.Machineconfiguration().V1().MachineOSConfigs(),
112120
ctrlctx.ConfigInformerFactory.Config().V1().ClusterVersions(),
113121
ctrlctx.InformerFactory.Machineconfiguration().V1alpha1().OSImageStreams(),
122+
iriInformer,
114123
ctrlctx,
115124
)
116125

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingAdmissionPolicy
3+
metadata:
4+
name: "internalreleaseimage-deletion-guard"
5+
spec:
6+
failurePolicy: Fail
7+
paramKind:
8+
apiVersion: config.openshift.io/v1
9+
kind: ClusterVersion
10+
matchConstraints:
11+
matchPolicy: Equivalent
12+
namespaceSelector: {}
13+
objectSelector: {}
14+
resourceRules:
15+
- apiGroups: ["machineconfiguration.openshift.io"]
16+
apiVersions: ["v1alpha1"]
17+
operations: ["DELETE"]
18+
resources: ["internalreleaseimages"]
19+
scope: "*"
20+
validations:
21+
- expression: "!oldObject.status.releases.exists(r, has(r.image) && r.image == params.status.desired.image)"
22+
message: "Cannot delete InternalReleaseImage while the cluster is using a release bundle from this resource. The current cluster release image matches a release stored in this InternalReleaseImage. Please upgrade or downgrade to a different release before deletion."
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: admissionregistration.k8s.io/v1
2+
kind: ValidatingAdmissionPolicyBinding
3+
metadata:
4+
name: "internalreleaseimage-deletion-guard-binding"
5+
spec:
6+
policyName: "internalreleaseimage-deletion-guard"
7+
validationActions: [Deny]
8+
paramRef:
9+
name: "version"
10+
parameterNotFoundAction: "Deny"

pkg/controller/internalreleaseimage/internalreleaseimage_controller.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020

2121
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
2222
mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1"
23+
configinformersv1 "github.com/openshift/client-go/config/informers/externalversions/config/v1"
24+
configlistersv1 "github.com/openshift/client-go/config/listers/config/v1"
2325
mcfgclientset "github.com/openshift/client-go/machineconfiguration/clientset/versioned"
2426
"github.com/openshift/client-go/machineconfiguration/clientset/versioned/scheme"
2527
mcfginformersv1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1"
@@ -28,6 +30,7 @@ import (
2830
mcfglistersv1alpha1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1alpha1"
2931
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
3032
templatectrl "github.com/openshift/machine-config-operator/pkg/controller/template"
33+
"github.com/openshift/machine-config-operator/pkg/osimagestream"
3134
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3235
)
3336

@@ -64,6 +67,9 @@ type Controller struct {
6467
mcLister mcfglistersv1.MachineConfigLister
6568
mcListerSynced cache.InformerSynced
6669

70+
clusterVersionLister configlistersv1.ClusterVersionLister
71+
clusterVersionListerSynced cache.InformerSynced
72+
6773
queue workqueue.TypedRateLimitingInterface[string]
6874
}
6975

@@ -72,6 +78,7 @@ func New(
7278
iriInformer mcfginformersv1alpha1.InternalReleaseImageInformer,
7379
ccInformer mcfginformersv1.ControllerConfigInformer,
7480
mcInformer mcfginformersv1.MachineConfigInformer,
81+
clusterVersionInformer configinformersv1.ClusterVersionInformer,
7582
kubeClient clientset.Interface,
7683
mcfgClient mcfgclientset.Interface,
7784
) *Controller {
@@ -115,6 +122,9 @@ func New(
115122
ctrl.mcLister = mcInformer.Lister()
116123
ctrl.mcListerSynced = mcInformer.Informer().HasSynced
117124

125+
ctrl.clusterVersionLister = clusterVersionInformer.Lister()
126+
ctrl.clusterVersionListerSynced = clusterVersionInformer.Informer().HasSynced
127+
118128
return ctrl
119129
}
120130

@@ -123,7 +133,7 @@ func (ctrl *Controller) Run(workers int, stopCh <-chan struct{}) {
123133
defer utilruntime.HandleCrash()
124134
defer ctrl.queue.ShutDown()
125135

126-
if !cache.WaitForCacheSync(stopCh, ctrl.iriListerSynced) {
136+
if !cache.WaitForCacheSync(stopCh, ctrl.iriListerSynced, ctrl.ccListerSynced, ctrl.mcListerSynced, ctrl.clusterVersionListerSynced) {
127137
return
128138
}
129139

@@ -336,6 +346,61 @@ func (ctrl *Controller) syncInternalReleaseImage(key string) error {
336346
}
337347
}
338348

349+
// Initialize status if empty
350+
if err := ctrl.initializeInternalReleaseImageStatus(iri); err != nil {
351+
return err
352+
}
353+
354+
return nil
355+
}
356+
357+
// initializeInternalReleaseImageStatus initializes the status of an InternalReleaseImage
358+
// if it is empty. It populates the status with release bundle entries from the spec,
359+
// setting the Image field from the current ClusterVersion and adding initial conditions.
360+
func (ctrl *Controller) initializeInternalReleaseImageStatus(iri *mcfgv1alpha1.InternalReleaseImage) error {
361+
// Only initialize if status is empty and spec has releases
362+
if len(iri.Status.Releases) != 0 || len(iri.Spec.Releases) == 0 {
363+
return nil
364+
}
365+
366+
klog.V(4).Infof("Initializing status for InternalReleaseImage %s", iri.Name)
367+
368+
// Get the release payload image from ClusterVersion
369+
releaseImage, err := osimagestream.GetReleasePayloadImage(ctrl.clusterVersionLister)
370+
if err != nil {
371+
return fmt.Errorf("error getting Release Image from ClusterVersion for InternalReleaseImage status initialization: %w", err)
372+
}
373+
374+
// Build status releases from spec releases
375+
statusReleases := make([]mcfgv1alpha1.InternalReleaseImageBundleStatus, 0, len(iri.Spec.Releases))
376+
for _, specRelease := range iri.Spec.Releases {
377+
statusRelease := mcfgv1alpha1.InternalReleaseImageBundleStatus{
378+
Name: specRelease.Name,
379+
Image: releaseImage,
380+
Conditions: []metav1.Condition{
381+
{
382+
Type: string(mcfgv1alpha1.InternalReleaseImageConditionTypeAvailable),
383+
Status: metav1.ConditionTrue,
384+
LastTransitionTime: metav1.Now(),
385+
Reason: "Installed",
386+
Message: "Release bundle is available",
387+
},
388+
},
389+
}
390+
statusReleases = append(statusReleases, statusRelease)
391+
}
392+
393+
iri.Status.Releases = statusReleases
394+
395+
// Update the status subresource
396+
if err := retry.RetryOnConflict(updateBackoff, func() error {
397+
_, err := ctrl.client.MachineconfigurationV1alpha1().InternalReleaseImages().UpdateStatus(context.TODO(), iri, metav1.UpdateOptions{})
398+
return err
399+
}); err != nil {
400+
return fmt.Errorf("failed to update InternalReleaseImage status: %w", err)
401+
}
402+
403+
klog.V(2).Infof("Initialized status for InternalReleaseImage %s with %d releases", iri.Name, len(statusReleases))
339404
return nil
340405
}
341406

pkg/controller/internalreleaseimage/internalreleaseimage_controller_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
99
mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1"
10+
configinformers "github.com/openshift/client-go/config/informers/externalversions"
1011
"github.com/openshift/client-go/machineconfiguration/clientset/versioned/fake"
1112
informers "github.com/openshift/client-go/machineconfiguration/informers/externalversions"
1213
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
@@ -18,6 +19,8 @@ import (
1819
"k8s.io/apimachinery/pkg/runtime"
1920
k8sfake "k8s.io/client-go/kubernetes/fake"
2021
"k8s.io/client-go/tools/record"
22+
23+
fakeconfigv1client "github.com/openshift/client-go/config/clientset/versioned/fake"
2124
)
2225

2326
func TestInternalReleaseImageCreate(t *testing.T) {
@@ -153,15 +156,18 @@ func TestInternalReleaseImageCreate(t *testing.T) {
153156
type fixture struct {
154157
t *testing.T
155158

156-
client *fake.Clientset
157-
k8sClient *k8sfake.Clientset
158-
iriLister []*mcfgv1alpha1.InternalReleaseImage
159-
ccLister []*mcfgv1.ControllerConfig
160-
mcLister []*mcfgv1.MachineConfig
161-
162-
controller *Controller
163-
objects []runtime.Object
164-
k8sObjects []runtime.Object
159+
client *fake.Clientset
160+
k8sClient *k8sfake.Clientset
161+
configClient *fakeconfigv1client.Clientset
162+
iriLister []*mcfgv1alpha1.InternalReleaseImage
163+
ccLister []*mcfgv1.ControllerConfig
164+
mcLister []*mcfgv1.MachineConfig
165+
166+
controller *Controller
167+
objects []runtime.Object
168+
k8sObjects []runtime.Object
169+
configObjects []runtime.Object
170+
configInformer configinformers.SharedInformerFactory
165171
}
166172

167173
func newFixture(t *testing.T, objects []runtime.Object) *fixture {
@@ -185,12 +191,15 @@ func (f *fixture) setupObjects(objs []runtime.Object) {
185191
func (f *fixture) newController() *Controller {
186192
f.client = fake.NewSimpleClientset(f.objects...)
187193
f.k8sClient = k8sfake.NewSimpleClientset(f.k8sObjects...)
194+
f.configClient = fakeconfigv1client.NewSimpleClientset(f.configObjects...)
188195
i := informers.NewSharedInformerFactory(f.client, func() time.Duration { return 0 }())
196+
f.configInformer = configinformers.NewSharedInformerFactory(f.configClient, func() time.Duration { return 0 }())
189197

190198
c := New(
191199
i.Machineconfiguration().V1alpha1().InternalReleaseImages(),
192200
i.Machineconfiguration().V1().ControllerConfigs(),
193201
i.Machineconfiguration().V1().MachineConfigs(),
202+
f.configInformer.Config().V1().ClusterVersions(),
194203
f.k8sClient,
195204
f.client,
196205
)
@@ -199,12 +208,15 @@ func (f *fixture) newController() *Controller {
199208
c.iriListerSynced = alwaysReady
200209
c.ccListerSynced = alwaysReady
201210
c.mcListerSynced = alwaysReady
211+
c.clusterVersionListerSynced = alwaysReady
202212
c.eventRecorder = &record.FakeRecorder{}
203213

204214
stopCh := make(chan struct{})
205215
defer close(stopCh)
206216
i.Start(stopCh)
207217
i.WaitForCacheSync(stopCh)
218+
f.configInformer.Start(stopCh)
219+
f.configInformer.WaitForCacheSync(stopCh)
208220

209221
for _, c := range f.iriLister {
210222
i.Machineconfiguration().V1alpha1().InternalReleaseImages().Informer().GetIndexer().Add(c)

pkg/operator/operator.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type Operator struct {
122122
apiserverLister configlistersv1.APIServerLister
123123
clusterVersionLister configlistersv1.ClusterVersionLister
124124
osImageStreamLister mcfglistersv1alpha1.OSImageStreamLister
125+
iriLister mcfglistersv1alpha1.InternalReleaseImageLister
125126

126127
crdListerSynced cache.InformerSynced
127128
deployListerSynced cache.InformerSynced
@@ -158,6 +159,7 @@ type Operator struct {
158159
moscListerSynced cache.InformerSynced
159160
apiserverListerSynced cache.InformerSynced
160161
osImageStreamListerSynced cache.InformerSynced
162+
iriListerSynced cache.InformerSynced
161163

162164
// queue only ever has one item, but it has nice error handling backoff/retry semantics
163165
queue workqueue.TypedRateLimitingInterface[string]
@@ -216,6 +218,7 @@ func New(
216218
moscInformer mcfginformersv1.MachineOSConfigInformer,
217219
clusterVersionInformer configinformersv1.ClusterVersionInformer,
218220
osImageStreamInformer mcfginformersv1alpha1.OSImageStreamInformer,
221+
iriInformer mcfginformersv1alpha1.InternalReleaseImageInformer,
219222
ctrlctx *ctrlcommon.ControllerContext,
220223
) *Operator {
221224
eventBroadcaster := record.NewBroadcaster()
@@ -365,6 +368,10 @@ func New(
365368
optr.osImageStreamLister = osImageStreamInformer.Lister()
366369
optr.osImageStreamListerSynced = osImageStreamInformer.Informer().HasSynced
367370
}
371+
if iriInformer != nil {
372+
optr.iriLister = iriInformer.Lister()
373+
optr.iriListerSynced = iriInformer.Informer().HasSynced
374+
}
368375

369376
optr.vStore.Set("operator", version.ReleaseVersion)
370377
optr.vStore.Set("operator-image", version.OperatorImage)
@@ -425,6 +432,9 @@ func (optr *Operator) Run(workers int, stopCh <-chan struct{}) {
425432
if optr.osImageStreamListerSynced != nil && osimagestream.IsFeatureEnabled(optr.fgHandler) {
426433
cacheSynced = append(cacheSynced, optr.osImageStreamListerSynced)
427434
}
435+
if optr.iriListerSynced != nil {
436+
cacheSynced = append(cacheSynced, optr.iriListerSynced)
437+
}
428438
if !cache.WaitForCacheSync(stopCh,
429439
cacheSynced...) {
430440
klog.Error("failed to sync caches")

pkg/operator/sync.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ const (
102102
mccUpdateBootImagesValidatingAdmissionPolicyBindingPath = "manifests/machineconfigcontroller/update-bootimages-validatingadmissionpolicybinding.yaml"
103103
mccMachineConfigPoolOSImageStreamValidatingAdmissionPolicyPath = "manifests/machineconfigcontroller/machineconfigpool-osimagestream-reference-validatingadmissionpolicy.yaml"
104104
mccMachineConfigPoolOSImageStreamValidatingAdmissionPolicyBindingPath = "manifests/machineconfigcontroller/machineconfigpool-osimagestream-reference-validatingadmissionpolicybinding.yaml"
105+
mccIRIDeletionGuardValidatingAdmissionPolicyPath = "manifests/machineconfigcontroller/internalreleaseimage-deletion-guard-validatingadmissionpolicy.yaml"
106+
mccIRIDeletionGuardValidatingAdmissionPolicyBindingPath = "manifests/machineconfigcontroller/internalreleaseimage-deletion-guard-validatingadmissionpolicybinding.yaml"
105107

106108
// Machine OS Builder manifest paths
107109
mobClusterRoleManifestPath = "manifests/machineosbuilder/clusterrole.yaml"
@@ -1187,6 +1189,16 @@ func (optr *Operator) syncMachineConfigController(config *renderConfig, _ *confi
11871189
paths.validatingAdmissionPolicyBindings = append(paths.validatingAdmissionPolicyBindings, mccMachineConfigPoolOSImageStreamValidatingAdmissionPolicyBindingPath)
11881190
}
11891191

1192+
if optr.fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
1193+
// Only deploy the IRI deletion guard policy if the IRI resource actually exists
1194+
if optr.iriLister != nil {
1195+
if _, err := optr.iriLister.Get(ctrlcommon.InternalReleaseImageInstanceName); err == nil {
1196+
paths.validatingAdmissionPolicies = append(paths.validatingAdmissionPolicies, mccIRIDeletionGuardValidatingAdmissionPolicyPath)
1197+
paths.validatingAdmissionPolicyBindings = append(paths.validatingAdmissionPolicyBindings, mccIRIDeletionGuardValidatingAdmissionPolicyBindingPath)
1198+
}
1199+
}
1200+
}
1201+
11901202
if err := optr.applyManifests(config, paths); err != nil {
11911203
return fmt.Errorf("failed to apply machine config controller manifests: %w", err)
11921204
}

0 commit comments

Comments
 (0)