Skip to content

Commit 186134b

Browse files
authored
Protect platform-auth-idp from upgrade clobber (#1033)
Originating issue: [IBMPrivateCloud/roadmap#66581](https://github.ibm.com/IBMPrivateCloud/roadmap/issues/66581) Also: * Avoid status conflicts in deferred func Signed-off-by: Rob Hundley <[email protected]>
1 parent a29848d commit 186134b

File tree

10 files changed

+272
-168
lines changed

10 files changed

+272
-168
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
Copyright 2025 IBM Corporation
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package bootstrap
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strconv"
23+
24+
operatorv1alpha1 "github.com/IBM/ibm-iam-operator/apis/operator/v1alpha1"
25+
ctrlcommon "github.com/IBM/ibm-iam-operator/controllers/common"
26+
"github.com/IBM/ibm-iam-operator/version"
27+
"github.com/opdev/subreconciler"
28+
corev1 "k8s.io/api/core/v1"
29+
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
30+
"k8s.io/apimachinery/pkg/types"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/builder"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/handler"
35+
logf "sigs.k8s.io/controller-runtime/pkg/log"
36+
"sigs.k8s.io/controller-runtime/pkg/predicate"
37+
)
38+
39+
// BootStrapReconciler handles modifications to the Authentication CR before it is reconciled by the
40+
// main Authentication controller
41+
type BootstrapReconciler struct {
42+
client.Client
43+
}
44+
45+
func (r *BootstrapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
46+
log := logf.FromContext(ctx).WithName("controller_authentication_bootstrap")
47+
subCtx := logf.IntoContext(ctx, log)
48+
if subResult, err := r.makeAuthenticationCorrections(subCtx, req); subreconciler.ShouldHaltOrRequeue(subResult, err) {
49+
if err != nil {
50+
log.Error(err, "An error was encountered during Authentication bootstrap")
51+
} else {
52+
log.Info("Needed to requeue during Authentication bootstrap")
53+
}
54+
return subreconciler.Evaluate(subResult, err)
55+
}
56+
log.Info("Authentication bootstrap successful")
57+
return
58+
}
59+
60+
// SetupWithManager sets up the controller with the Manager.
61+
func (r *BootstrapReconciler) SetupWithManager(mgr ctrl.Manager) error {
62+
63+
authCtrl := ctrl.NewControllerManagedBy(mgr)
64+
65+
bootstrapPred := predicate.NewPredicateFuncs(func(o client.Object) bool {
66+
return o.GetLabels()[ctrlcommon.ManagerVersionLabel] != version.Version
67+
})
68+
69+
authCtrl.Watches(&operatorv1alpha1.Authentication{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(bootstrapPred))
70+
return authCtrl.Named("controller_authentication_bootstrap").Complete(r)
71+
}
72+
73+
// makeAuthenticationCorrections handles changes that need to happen to the Authentication CR for compatibility reasons.
74+
func (r *BootstrapReconciler) makeAuthenticationCorrections(ctx context.Context, req ctrl.Request) (result *ctrl.Result, err error) {
75+
log := logf.FromContext(ctx)
76+
authCR := &operatorv1alpha1.Authentication{}
77+
if result, err = r.getLatestAuthentication(ctx, req, authCR); subreconciler.ShouldHaltOrRequeue(result, err) {
78+
return
79+
}
80+
if authCR.Labels[ctrlcommon.ManagerVersionLabel] == version.Version {
81+
return subreconciler.ContinueReconciling()
82+
}
83+
84+
if needsAuditServiceDummyDataReset(authCR) {
85+
authCR.SetRequiredDummyData()
86+
}
87+
88+
if err = r.writeConfigurationsToAuthenticationCR(ctx, authCR); err != nil {
89+
log.Error(err, "Failed to update the Authentication")
90+
return subreconciler.RequeueWithError(err)
91+
}
92+
93+
authCR.Labels[ctrlcommon.ManagerVersionLabel] = version.Version
94+
95+
err = r.Update(ctx, authCR)
96+
if err != nil {
97+
log.Error(err, "Failed to update the Authentication")
98+
return subreconciler.RequeueWithError(err)
99+
}
100+
log.Info("Performed bootstrap update to Authentication successfully")
101+
102+
return subreconciler.ContinueReconciling()
103+
}
104+
105+
// writeConfigurationsToAuthenticationCR copies values from the
106+
// platform-auth-idp ConfigMap to the Authentication CR.
107+
func (r *BootstrapReconciler) writeConfigurationsToAuthenticationCR(ctx context.Context, authCR *operatorv1alpha1.Authentication) (err error) {
108+
platformAuthIDPCM := &corev1.ConfigMap{}
109+
if err = r.Get(ctx, types.NamespacedName{Name: "platform-auth-idp", Namespace: authCR.Namespace}, platformAuthIDPCM); k8sErrors.IsNotFound(err) {
110+
return nil
111+
} else if err != nil {
112+
return fmt.Errorf("failed to get ConfigMap: %w", err)
113+
}
114+
keys := map[string]any{
115+
"ROKS_URL": &authCR.Spec.Config.ROKSURL,
116+
"ROKS_USER_PREFIX": &authCR.Spec.Config.ROKSUserPrefix,
117+
"ROKS_ENABLED": &authCR.Spec.Config.ROKSEnabled,
118+
"BOOTSTRAP_USERID": &authCR.Spec.Config.BootstrapUserId,
119+
"CLAIMS_SUPPORTED": &authCR.Spec.Config.ClaimsSupported,
120+
"CLAIMS_MAP": &authCR.Spec.Config.ClaimsMap,
121+
"DEFAULT_LOGIN": &authCR.Spec.Config.DefaultLogin,
122+
"SCOPE_CLAIM": &authCR.Spec.Config.ScopeClaim,
123+
"NONCE_ENABLED": &authCR.Spec.Config.NONCEEnabled,
124+
"PREFERRED_LOGIN": &authCR.Spec.Config.PreferredLogin,
125+
"OIDC_ISSUER_URL": &authCR.Spec.Config.OIDCIssuerURL,
126+
"PROVIDER_ISSUER_URL": &authCR.Spec.Config.ProviderIssuerURL,
127+
"CLUSTER_NAME": &authCR.Spec.Config.ClusterName,
128+
"FIPS_ENABLED": &authCR.Spec.Config.FIPSEnabled,
129+
"IBM_CLOUD_SAAS": &authCR.Spec.Config.IBMCloudSaas,
130+
"SAAS_CLIENT_REDIRECT_URL": &authCR.Spec.Config.SaasClientRedirectUrl,
131+
"ATTR_MAPPING_FROM_CONFIG": &authCR.Spec.Config.AttrMappingFromConfig,
132+
}
133+
134+
for key, crField := range keys {
135+
cmValue, ok := platformAuthIDPCM.Data[key]
136+
if !ok {
137+
continue
138+
}
139+
switch crValue := crField.(type) {
140+
case *string:
141+
if *crValue != cmValue {
142+
*crValue = cmValue
143+
}
144+
case *bool:
145+
cmValueBool, _ := strconv.ParseBool(cmValue)
146+
if *crValue != cmValueBool {
147+
*crValue = cmValueBool
148+
}
149+
}
150+
}
151+
152+
return
153+
}
154+
155+
// needsAuditServiceDummyDataReset compares the state in an Authentication's .spec.auditService and returns whether it
156+
// needs to be overwritten with dummy data.
157+
func needsAuditServiceDummyDataReset(a *operatorv1alpha1.Authentication) bool {
158+
return a.Spec.AuditService.ImageName != operatorv1alpha1.AuditServiceIgnoreString ||
159+
a.Spec.AuditService.ImageRegistry != operatorv1alpha1.AuditServiceIgnoreString ||
160+
a.Spec.AuditService.ImageTag != operatorv1alpha1.AuditServiceIgnoreString ||
161+
a.Spec.AuditService.SyslogTlsPath != "" ||
162+
a.Spec.AuditService.Resources != nil
163+
}
164+
165+
func (r *BootstrapReconciler) getLatestAuthentication(ctx context.Context, req ctrl.Request, authentication *operatorv1alpha1.Authentication) (result *ctrl.Result, err error) {
166+
reqLogger := logf.FromContext(ctx)
167+
if err := r.Get(ctx, req.NamespacedName, authentication); err != nil {
168+
if k8sErrors.IsNotFound(err) {
169+
reqLogger.Info("Authentication not found; skipping reconciliation")
170+
return subreconciler.DoNotRequeue()
171+
}
172+
reqLogger.Error(err, "Failed to get Authentication")
173+
return subreconciler.RequeueWithError(err)
174+
}
175+
return subreconciler.ContinueReconciling()
176+
}

controllers/common/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ const (
4545
PlatformIdentityManagement DeploymentName = "platform-identity-management"
4646
PlatformAuthService DeploymentName = "platform-auth-service"
4747
)
48+
49+
const ManagerVersionLabel string = "authentication.operator.ibm.com/manager-version"

controllers/common/utils.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23-
"io/ioutil"
2423
"os"
2524
"strings"
2625
"time"
@@ -67,7 +66,7 @@ var _ fmt.Stringer = OpenShift
6766

6867
// GetClusterType attempts to determine whether the Operator is running on Openshift versus a CNCF cluster. Exits in the
6968
// event that the cluster config can't be obtained to make queries or if the watch namespace can't be obtained.
70-
func GetClusterType(ctx context.Context, k8sClient *client.Client, cmName string) (clusterType ClusterType, err error) {
69+
func GetClusterType(ctx context.Context, k8sClient client.Client, cmName string) (clusterType ClusterType, err error) {
7170
logger := logf.FromContext(ctx)
7271
logger.Info("Get cluster config")
7372

@@ -101,7 +100,7 @@ func GetClusterType(ctx context.Context, k8sClient *client.Client, cmName string
101100
cm := &corev1.ConfigMap{}
102101
for _, namespace := range namespaces {
103102
logger.Info("Try to find ConfigMap", "name", cmName, "namespace", namespace)
104-
err = (*k8sClient).Get(ctx, types.NamespacedName{Name: cmName, Namespace: namespace}, cm)
103+
err = k8sClient.Get(ctx, types.NamespacedName{Name: cmName, Namespace: namespace}, cm)
105104
if err != nil {
106105
logger.Error(err, "Failed to get ConfigMap")
107106
logger.Info("Did not find it", "namespace", namespace)
@@ -265,7 +264,7 @@ func GetOperatorNamespace() (string, error) {
265264
} else if isRunModeLocal() {
266265
return "", ErrRunLocal
267266
}
268-
nsBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
267+
nsBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
269268
if err != nil {
270269
if os.IsNotExist(err) {
271270
return "", ErrNoNamespace
@@ -283,22 +282,38 @@ func GetOperatorNamespace() (string, error) {
283282
// Authentication CR for a given IM Operator instance, so wherever that one Authentication CR is found is assumed to be
284283
// where the other Operands are. If no or more than one Authentication CR is found, an error is reported as this is an
285284
// unsupported usage of the CR.
286-
func GetAuthentication(ctx context.Context, k8sClient *client.Client) (authCR *operatorv1alpha1.Authentication, err error) {
287-
authenticationList := &operatorv1alpha1.AuthenticationList{}
288-
err = (*k8sClient).List(ctx, authenticationList)
289-
if err != nil {
290-
return
285+
func GetAuthentication(ctx context.Context, k8sClient client.Client, namespaces ...string) (authCR *operatorv1alpha1.Authentication, err error) {
286+
if len(namespaces) == 0 {
287+
authenticationList := &operatorv1alpha1.AuthenticationList{}
288+
err = k8sClient.List(ctx, authenticationList)
289+
if err != nil {
290+
return
291+
}
292+
if len(authenticationList.Items) != 1 {
293+
err = fmt.Errorf("expected to find 1 Authentication but found %d", len(authenticationList.Items))
294+
return
295+
}
296+
return &authenticationList.Items[0], nil
291297
}
292-
if len(authenticationList.Items) != 1 {
293-
err = fmt.Errorf("expected to find 1 Authentication but found %d", len(authenticationList.Items))
294-
return
298+
299+
var errs error = nil
300+
authCR = &operatorv1alpha1.Authentication{}
301+
for _, namespace := range namespaces {
302+
err = k8sClient.Get(ctx, types.NamespacedName{Name: "example-authentication", Namespace: namespace}, authCR)
303+
if k8sErrors.IsNotFound(err) {
304+
continue
305+
} else if err == nil {
306+
return
307+
}
308+
errs = errors.Join(errs, err)
295309
}
296-
return &authenticationList.Items[0], nil
310+
311+
return nil, errs
297312
}
298313

299314
// GetServicesNamespace finds the namespace that contains the shared services deriving from the Authentication CR for
300315
// this IM install. Returns an error when
301-
func GetServicesNamespace(ctx context.Context, k8sClient *client.Client) (namespace string, err error) {
316+
func GetServicesNamespace(ctx context.Context, k8sClient client.Client) (namespace string, err error) {
302317
authCR, err := GetAuthentication(ctx, k8sClient)
303318
if err != nil {
304319
return

controllers/oidc.security/client_controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func (r *ClientReconciler) confirmAuthenticationIsReady(ctx context.Context, req
127127
reqLogger.Info("Confirm that Authentication CR for IM is ready before reconciling Client")
128128

129129
var authCR *operatorv1alpha1.Authentication
130-
if authCR, err = common.GetAuthentication(ctx, &r.Client); err != nil {
130+
if authCR, err = common.GetAuthentication(ctx, r.Client); err != nil {
131131
reqLogger.Info("Failed to get the Authentication for this install of IM", "reason", err.Error())
132132
return subreconciler.RequeueWithDelay(wait.Jitter(baseAuthenticationWaitTime, 1.0))
133133
}
@@ -429,7 +429,7 @@ func (r *ClientReconciler) annotateServiceAccount(ctx context.Context, req ctrl.
429429
}
430430

431431
sAccName := "ibm-iam-operand-restricted"
432-
sAccNamespace, err := common.GetServicesNamespace(ctx, &r.Client)
432+
sAccNamespace, err := common.GetServicesNamespace(ctx, r.Client)
433433
if err != nil {
434434
return subreconciler.RequeueWithError(err)
435435
}
@@ -824,7 +824,7 @@ func (r *ClientReconciler) removeAnnotationFromSA(ctx context.Context, req ctrl.
824824
}
825825

826826
sAccName := "ibm-iam-operand-restricted"
827-
sAccNamespace, err := common.GetServicesNamespace(ctx, &r.Client)
827+
sAccNamespace, err := common.GetServicesNamespace(ctx, r.Client)
828828
if err != nil {
829829
reqLogger.Error(err, "Failed to get services namespace")
830830
return subreconciler.RequeueWithError(err)

controllers/oidc.security/client_controller_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (c AuthenticationConfig) GetCSCATLSKey() (value []byte, err error) {
280280
}
281281

282282
func GetConfig(ctx context.Context, r *ClientReconciler, config *AuthenticationConfig) (err error) {
283-
servicesNamespace, err := ctrlCommon.GetServicesNamespace(ctx, &r.Client)
283+
servicesNamespace, err := ctrlCommon.GetServicesNamespace(ctx, r.Client)
284284
if err != nil {
285285
return fmt.Errorf("failed to get ConfigMap: %w", err)
286286
}

controllers/oidc.security/clientreg.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func (r *ClientReconciler) invokeClientRegistrationAPI(ctx context.Context, clie
232232
// return whatever matching Secret exists in the cluster and cache it for future use.
233233
func (r *ClientReconciler) getCSCACertificateSecret(ctx context.Context) (secret *corev1.Secret, err error) {
234234
secret = &corev1.Secret{}
235-
servicesNamespace, err := common.GetServicesNamespace(ctx, &r.Client)
235+
servicesNamespace, err := common.GetServicesNamespace(ctx, r.Client)
236236
if err != nil {
237237
return
238238
}

0 commit comments

Comments
 (0)