diff --git a/go.mod b/go.mod index 799697bf72..96e3ed6c33 100644 --- a/go.mod +++ b/go.mod @@ -128,3 +128,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace github.com/openshift/library-go => github.com/openshift-cherrypick-robot/library-go v0.0.0-20250912134350-65142f98d552 //cherry-pick-1936-to-release-4.19 diff --git a/go.sum b/go.sum index 449c12dd14..569866c938 100644 --- a/go.sum +++ b/go.sum @@ -160,14 +160,14 @@ github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/openshift-cherrypick-robot/library-go v0.0.0-20250912134350-65142f98d552 h1:YH3UKjcREsQbXEhGbTXIermM06eCCdvv8p0HHMyrDjc= +github.com/openshift-cherrypick-robot/library-go v0.0.0-20250912134350-65142f98d552/go.mod h1:DAa3BGl0CFtkfJn/g5rU8kDDTErfMVA/QlFm4cvU+MI= github.com/openshift/api v0.0.0-20250320170726-75d64d71980b h1:GGuFSHESP0BSOu70AqV4u9IVrjYdaeu4Id+HXRIOvkw= github.com/openshift/api v0.0.0-20250320170726-75d64d71980b/go.mod h1:yk60tHAmHhtVpJQo3TwVYq2zpuP70iJIFDCmeKMIzPw= github.com/openshift/build-machinery-go v0.0.0-20250102153059-e85a1a7ecb5c h1:6XcszPFZpan4qll5XbdLll7n1So3IsPn28aw2j1obMo= github.com/openshift/build-machinery-go v0.0.0-20250102153059-e85a1a7ecb5c/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20250125113824-8e1f0b8fa9a7 h1:4iliLcvr1P9EUMZgIaSNEKNQQzBn+L6PSequlFOuB6Q= github.com/openshift/client-go v0.0.0-20250125113824-8e1f0b8fa9a7/go.mod h1:2tcufBE4Cu6RNgDCxcUJepa530kGo5GFVfR9BSnndhI= -github.com/openshift/library-go v0.0.0-20250512121900-863508cf7a27 h1:9gC5e5821g15dahkbhKd8njBU8l/3l+0LgGvlMKWs2I= -github.com/openshift/library-go v0.0.0-20250512121900-863508cf7a27/go.mod h1:DAa3BGl0CFtkfJn/g5rU8kDDTErfMVA/QlFm4cvU+MI= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/operator/targetconfigcontroller/targetconfigcontroller.go b/pkg/operator/targetconfigcontroller/targetconfigcontroller.go index 3ac9a42dd1..b0778c9ee4 100644 --- a/pkg/operator/targetconfigcontroller/targetconfigcontroller.go +++ b/pkg/operator/targetconfigcontroller/targetconfigcontroller.go @@ -13,7 +13,6 @@ import ( "github.com/ghodss/yaml" - "github.com/openshift/api/annotations" kubecontrolplanev1 "github.com/openshift/api/kubecontrolplane/v1" operatorv1 "github.com/openshift/api/operator/v1" "github.com/openshift/cluster-kube-apiserver-operator/bindata" @@ -24,6 +23,7 @@ import ( "github.com/openshift/library-go/pkg/operator/certrotation" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + "github.com/openshift/library-go/pkg/operator/resource/resourcehelper" "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" "github.com/openshift/library-go/pkg/operator/resource/resourceread" "github.com/openshift/library-go/pkg/operator/resourcesynccontroller" @@ -38,6 +38,7 @@ import ( "k8s.io/client-go/kubernetes" coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/klog/v2" ) const ( @@ -347,12 +348,33 @@ func generateOptionalStartupMonitorPod(isStartupMonitorEnabledFn func() (bool, e } func ManageClientCABundle(ctx context.Context, lister corev1listers.ConfigMapLister, client coreclientv1.ConfigMapsGetter, recorder events.Recorder) (*corev1.ConfigMap, bool, error) { - requiredConfigMap, err := resourcesynccontroller.CombineCABundleConfigMaps( - resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: "client-ca"}, + + additionalAnnotations := certrotation.AdditionalAnnotations{ + JiraComponent: "kube-apiserver", + } + caBundleConfigMapName := "client-ca" + + creationRequired := false + updateRequired := false + + caBundleConfigMap, err := lister.ConfigMaps(operatorclient.TargetNamespace).Get(caBundleConfigMapName) + switch { + case apierrors.IsNotFound(err): + creationRequired = true + caBundleConfigMap = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: caBundleConfigMapName, + Namespace: operatorclient.TargetNamespace, + }, + } + case err != nil: + return nil, false, err + } + + requiredConfigMap, updateRequired, err := resourcesynccontroller.CombineCABundleConfigMapsOptimistically( + caBundleConfigMap, lister, - certrotation.AdditionalAnnotations{ - JiraComponent: "kube-apiserver", - }, + additionalAnnotations, // this is from the installer and contains the value to verify the admin.kubeconfig user resourcesynccontroller.ResourceLocation{Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, Name: "admin-kubeconfig-client-ca"}, // this is from the installer and contains the value to verify the node bootstrapping cert that is baked into images @@ -372,21 +394,56 @@ func ManageClientCABundle(ctx context.Context, lister corev1listers.ConfigMapLis if err != nil { return nil, false, err } - if requiredConfigMap.Annotations == nil { - requiredConfigMap.Annotations = map[string]string{} + + if creationRequired { + caBundleConfigMap, err = client.ConfigMaps(operatorclient.TargetNamespace).Create(ctx, requiredConfigMap, metav1.CreateOptions{}) + resourcehelper.ReportCreateEvent(recorder, caBundleConfigMap, err) + if err != nil { + return nil, false, err + } + klog.V(2).Infof("Created client CA bundle configmap %s/%s", caBundleConfigMap.Namespace, caBundleConfigMap.Name) + return caBundleConfigMap, true, nil + } else if updateRequired { + caBundleConfigMap, err = client.ConfigMaps(operatorclient.TargetNamespace).Update(ctx, requiredConfigMap, metav1.UpdateOptions{}) + resourcehelper.ReportUpdateEvent(recorder, caBundleConfigMap, err) + if err != nil { + return nil, false, err + } + klog.V(2).Infof("Updated client CA bundle configmap %s/%s", caBundleConfigMap.Namespace, caBundleConfigMap.Name) + return caBundleConfigMap, true, nil } - requiredConfigMap.Annotations[annotations.OpenShiftComponent] = "kube-apiserver" - return resourceapply.ApplyConfigMap(ctx, client, recorder, requiredConfigMap) + return caBundleConfigMap, false, nil } func manageKubeAPIServerCABundle(ctx context.Context, lister corev1listers.ConfigMapLister, client coreclientv1.ConfigMapsGetter, recorder events.Recorder) (*corev1.ConfigMap, bool, error) { - requiredConfigMap, err := resourcesynccontroller.CombineCABundleConfigMaps( - resourcesynccontroller.ResourceLocation{Namespace: operatorclient.TargetNamespace, Name: "kube-apiserver-server-ca"}, + + additionalAnnotations := certrotation.AdditionalAnnotations{ + JiraComponent: "kube-apiserver", + } + caBundleConfigMapName := "kube-apiserver-server-ca" + + creationRequired := false + updateRequired := false + + caBundleConfigMap, err := lister.ConfigMaps(operatorclient.TargetNamespace).Get(caBundleConfigMapName) + switch { + case apierrors.IsNotFound(err): + creationRequired = true + caBundleConfigMap = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: caBundleConfigMapName, + Namespace: operatorclient.TargetNamespace, + }, + } + case err != nil: + return nil, false, err + } + + requiredConfigMap, updateRequired, err := resourcesynccontroller.CombineCABundleConfigMapsOptimistically( + caBundleConfigMap, lister, - certrotation.AdditionalAnnotations{ - JiraComponent: "kube-apiserver", - }, + additionalAnnotations, // this bundle is what this operator uses to mint loadbalancers certs resourcesynccontroller.ResourceLocation{Namespace: operatorclient.OperatorNamespace, Name: "loadbalancer-serving-ca"}, // this bundle is what this operator uses to mint localhost certs @@ -399,12 +456,26 @@ func manageKubeAPIServerCABundle(ctx context.Context, lister corev1listers.Confi if err != nil { return nil, false, err } - if requiredConfigMap.Annotations == nil { - requiredConfigMap.Annotations = map[string]string{} + + if creationRequired { + caBundleConfigMap, err := client.ConfigMaps(operatorclient.TargetNamespace).Create(ctx, requiredConfigMap, metav1.CreateOptions{}) + resourcehelper.ReportCreateEvent(recorder, caBundleConfigMap, err) + if err != nil { + return nil, false, err + } + klog.V(2).Infof("Created kube apiserver CA bundle configmap %s/%s", caBundleConfigMap.Namespace, caBundleConfigMap.Name) + return caBundleConfigMap, true, nil + } else if updateRequired { + caBundleConfigMap, err := client.ConfigMaps(operatorclient.TargetNamespace).Update(ctx, requiredConfigMap, metav1.UpdateOptions{}) + resourcehelper.ReportUpdateEvent(recorder, caBundleConfigMap, err) + if err != nil { + return nil, false, err + } + klog.V(2).Infof("Updated kube apiserver CA bundle configmap %s/%s", caBundleConfigMap.Namespace, caBundleConfigMap.Name) + return caBundleConfigMap, true, nil } - requiredConfigMap.Annotations[annotations.OpenShiftComponent] = "kube-apiserver" - return resourceapply.ApplyConfigMap(ctx, client, recorder, requiredConfigMap) + return caBundleConfigMap, false, nil } func ensureKubeAPIServerTrustedCA(ctx context.Context, client coreclientv1.CoreV1Interface, recorder events.Recorder) error { diff --git a/pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go b/pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go index db757173e3..09904af22c 100644 --- a/pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go +++ b/pkg/operator/targetconfigcontroller/targetconfigcontroller_test.go @@ -2,12 +2,18 @@ package targetconfigcontroller import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" + "math/big" "strconv" "strings" "testing" + "time" - operatorv1 "github.com/openshift/api/operator/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -15,6 +21,13 @@ import ( "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/utils/clock" + + "github.com/openshift/api/annotations" + operatorv1 "github.com/openshift/api/operator/v1" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/stretchr/testify/require" ) var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) @@ -462,3 +475,604 @@ func (l *configMapLister) ConfigMaps(namespace string) corev1listers.ConfigMapNa func (l *configMapLister) Get(name string) (*corev1.ConfigMap, error) { return l.client.CoreV1().ConfigMaps(l.namespace).Get(context.Background(), name, metav1.GetOptions{}) } + +func TestManageClientCABundle(t *testing.T) { + cert1, err := generateTemporaryCertificate() + require.NoError(t, err) + + cert2, err := generateTemporaryCertificate() + require.NoError(t, err) + + tests := []struct { + name string + existingConfigMaps []*corev1.ConfigMap + expectedConfigMap *corev1.ConfigMap + expectedChanged bool + }{ + { + name: "create new client-ca configmap when none exists", + existingConfigMaps: []*corev1.ConfigMap{}, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": "", + }, + }, + expectedChanged: true, + }, + { + name: "one source", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-kubeconfig-client-ca", + Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "set annotations if missing", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{}, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-kubeconfig-client-ca", + Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "annotations update", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-kubeconfig-client-ca", + Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + "foo": "bar", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "update existing client-ca configmap when new source appears", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-kubeconfig-client-ca", + Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + // Add a new source that wasn't in the original bundle + { + ObjectMeta: metav1.ObjectMeta{ + Name: "csr-controller-ca", + Namespace: operatorclient.GlobalMachineSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert2), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1) + string(cert2), + }, + }, + expectedChanged: true, + }, + { + name: "no changes required", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "admin-kubeconfig-client-ca", + Namespace: operatorclient.GlobalUserSpecifiedConfigNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "client-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := fake.NewSimpleClientset() + + // Create existing configmaps + for _, cm := range test.existingConfigMaps { + _, err := client.CoreV1().ConfigMaps(cm.Namespace).Create(context.Background(), cm, metav1.CreateOptions{}) + require.NoError(t, err) + } + + lister := &configMapLister{ + client: client, + namespace: "", + } + + recorder := events.NewInMemoryRecorder("test", clock.RealClock{}) + + // Call the function under test + resultConfigMap, changed, err := ManageClientCABundle(context.Background(), lister, client.CoreV1(), recorder) + + // Assert error expectations + require.NoError(t, err) + + // Assert change expectations + require.Equal(t, test.expectedChanged, changed, "Expected changed=%v, got changed=%v", test.expectedChanged, changed) + + // Compare with expected configmap + require.Equal(t, test.expectedConfigMap, resultConfigMap) + + // Verify the configmap exists in the cluster + storedConfigMap, err := client.CoreV1().ConfigMaps(operatorclient.TargetNamespace).Get(context.Background(), "client-ca", metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, storedConfigMap) + + // Ensure the returned configmap matches what's stored in the cluster + require.Equal(t, storedConfigMap, resultConfigMap, "returned configmap should match stored configmap") + + // Verify events were recorded if changes were made + if test.expectedChanged { + events := recorder.Events() + require.NotEmpty(t, events) + } + }) + } +} + +func TestManageKubeAPIServerCABundle(t *testing.T) { + cert1, err := generateTemporaryCertificate() + require.NoError(t, err) + + cert2, err := generateTemporaryCertificate() + require.NoError(t, err) + + tests := []struct { + name string + existingConfigMaps []*corev1.ConfigMap + expectedConfigMap *corev1.ConfigMap + expectedChanged bool + }{ + { + name: "create new kube-apiserver-server-ca configmap when none exists", + existingConfigMaps: []*corev1.ConfigMap{}, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": "", + }, + }, + expectedChanged: true, + }, + { + name: "one source", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "set annotations if missing", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{}, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "annotations update", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + "foo": "bar", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + "foo": "bar", + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: true, + }, + { + name: "update existing kube-apiserver-server-ca configmap when new source appears", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + // Add a new source that wasn't in the original bundle + { + ObjectMeta: metav1.ObjectMeta{ + Name: "localhost-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert2), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1) + string(cert2), + }, + }, + expectedChanged: true, + }, + { + name: "no changes required", + existingConfigMaps: []*corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "loadbalancer-serving-ca", + Namespace: operatorclient.OperatorNamespace, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + }, + expectedConfigMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver-server-ca", + Namespace: operatorclient.TargetNamespace, + Annotations: map[string]string{ + annotations.OpenShiftComponent: "kube-apiserver", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": string(cert1), + }, + }, + expectedChanged: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := fake.NewSimpleClientset() + + // Create existing configmaps + for _, cm := range test.existingConfigMaps { + _, err := client.CoreV1().ConfigMaps(cm.Namespace).Create(context.Background(), cm, metav1.CreateOptions{}) + require.NoError(t, err) + } + + lister := &configMapLister{ + client: client, + namespace: "", + } + + recorder := events.NewInMemoryRecorder("test", clock.RealClock{}) + + // Call the function under test + resultConfigMap, changed, err := manageKubeAPIServerCABundle(context.Background(), lister, client.CoreV1(), recorder) + + // Assert error expectations + require.NoError(t, err) + + // Assert change expectations + require.Equal(t, test.expectedChanged, changed, "Expected changed=%v, got changed=%v", test.expectedChanged, changed) + + // Compare with expected configmap + require.Equal(t, test.expectedConfigMap, resultConfigMap) + + // Verify the configmap exists in the cluster + storedConfigMap, err := client.CoreV1().ConfigMaps(operatorclient.TargetNamespace).Get(context.Background(), "kube-apiserver-server-ca", metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, storedConfigMap) + + // Ensure the returned configmap matches what's stored in the cluster + require.Equal(t, storedConfigMap, resultConfigMap, "returned configmap should match stored configmap") + + // Verify events were recorded if changes were made + if test.expectedChanged { + events := recorder.Events() + require.NotEmpty(t, events) + } + }) + } +} + +// generateTemporaryCertificate creates a new temporary, self-signed x509 certificate +// and a corresponding RSA private key. The certificate will be valid for 24 hours. +// It returns the PEM-encoded private key and certificate. +func generateTemporaryCertificate() (certPEM []byte, err error) { + // 1. Generate a new RSA private key + // We are using a 2048-bit key, which is a common and secure choice. + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + // 2. Create a template for the certificate + // This template contains all the details about the certificate. + certTemplate := x509.Certificate{ + // SerialNumber is a unique number for the certificate. + // We generate a large random number to ensure uniqueness. + SerialNumber: big.NewInt(time.Now().Unix()), + + // Subject contains information about the owner of the certificate. + Subject: pkix.Name{ + Organization: []string{"My Company, Inc."}, + Country: []string{"US"}, + Province: []string{"California"}, + Locality: []string{"San Francisco"}, + CommonName: "localhost", // Common Name (CN) + }, + + // NotBefore is the start time of the certificate's validity. + NotBefore: time.Now(), + // NotAfter is the end time. We set it to 24 hours from now. + NotAfter: time.Now().Add(24 * time.Hour), + + // KeyUsage defines the purpose of the public key contained in the certificate. + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + // ExtKeyUsage indicates extended purposes (e.g., server/client authentication). + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + + // BasicConstraintsValid indicates if this is a CA certificate. + // Since this is a self-signed certificate, we set it to true. + BasicConstraintsValid: true, + } + + // 3. Create the certificate + // x509.CreateCertificate creates a new certificate based on a template. + // Since this is a self-signed certificate, the parent certificate is the template itself. + // We use the public key from our generated private key. + // The final argument is the private key used to sign the certificate. + certBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + // 4. Encode the certificate to the PEM format + certPEM = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + return certPEM, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go index 80f5efc2c0..33a09ae16e 100644 --- a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go +++ b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go @@ -629,7 +629,7 @@ func MakeSelfSignedCAConfig(name string, lifetime time.Duration) (*TLSCertificat func MakeSelfSignedCAConfigForSubject(subject pkix.Name, lifetime time.Duration) (*TLSCertificateConfig, error) { if lifetime <= 0 { lifetime = DefaultCACertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCACertificateLifetimeDuration { @@ -1018,7 +1018,7 @@ func newSigningCertificateTemplateForDuration(subject pkix.Name, caLifetime time func newServerCertificateTemplate(subject pkix.Name, hosts []string, lifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte) *x509.Certificate { if lifetime <= 0 { lifetime = DefaultCertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCertificateLifetimeDuration { @@ -1105,7 +1105,7 @@ func CertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) { func NewClientCertificateTemplate(subject pkix.Name, lifetime time.Duration, currentTime func() time.Time) *x509.Certificate { if lifetime <= 0 { lifetime = DefaultCertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCertificateLifetimeDuration { diff --git a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go index fa9709ec06..7fcd4a9aa9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/certrotation/annotations.go @@ -28,19 +28,19 @@ func (a AdditionalAnnotations) EnsureTLSMetadataUpdate(meta *metav1.ObjectMeta) } if len(a.JiraComponent) > 0 && meta.Annotations[annotations.OpenShiftComponent] != a.JiraComponent { diff := cmp.Diff(meta.Annotations[annotations.OpenShiftComponent], a.JiraComponent) - klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", annotations.OpenShiftComponent, meta.Name, meta.Namespace, diff) + klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", annotations.OpenShiftComponent, meta.Namespace, meta.Name, diff) meta.Annotations[annotations.OpenShiftComponent] = a.JiraComponent modified = true } if len(a.Description) > 0 && meta.Annotations[annotations.OpenShiftDescription] != a.Description { diff := cmp.Diff(meta.Annotations[annotations.OpenShiftDescription], a.Description) - klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", annotations.OpenShiftDescription, meta.Name, meta.Namespace, diff) + klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", annotations.OpenShiftDescription, meta.Namespace, meta.Name, diff) meta.Annotations[annotations.OpenShiftDescription] = a.Description modified = true } if len(a.AutoRegenerateAfterOfflineExpiry) > 0 && meta.Annotations[AutoRegenerateAfterOfflineExpiryAnnotation] != a.AutoRegenerateAfterOfflineExpiry { diff := cmp.Diff(meta.Annotations[AutoRegenerateAfterOfflineExpiryAnnotation], a.AutoRegenerateAfterOfflineExpiry) - klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", AutoRegenerateAfterOfflineExpiryAnnotation, meta.Name, meta.Namespace, diff) + klog.V(2).Infof("Updating %q annotation for %s/%s, diff: %s", AutoRegenerateAfterOfflineExpiryAnnotation, meta.Namespace, meta.Name, diff) meta.Annotations[AutoRegenerateAfterOfflineExpiryAnnotation] = a.AutoRegenerateAfterOfflineExpiry modified = true } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go index 357efad619..24ce3ec3a7 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go @@ -208,6 +208,18 @@ func ApplyDirectly(ctx context.Context, clients *ClientHolder, recorder events.R } else { result.Result, result.Changed, result.Error = ApplyValidatingAdmissionPolicyBindingV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t, cache) } + case *admissionregistrationv1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + result.Result, result.Changed, result.Error = ApplyValidatingAdmissionPolicyV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t, cache) + } + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + result.Result, result.Changed, result.Error = ApplyValidatingAdmissionPolicyBindingV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t, cache) + } case *storagev1.CSIDriver: if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go index 3199d2db05..d44a5d571a 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go @@ -142,6 +142,9 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter if required.Annotations == nil { required.Annotations = map[string]string{} } + if required.Labels == nil { + required.Labels = map[string]string{} + } if err := SetSpecHashAnnotation(&required.ObjectMeta, required.Spec); err != nil { return nil, false, err } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resourcesynccontroller/core.go b/vendor/github.com/openshift/library-go/pkg/operator/resourcesynccontroller/core.go index 9711348b33..bbd2ab58fb 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resourcesynccontroller/core.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resourcesynccontroller/core.go @@ -70,3 +70,64 @@ func CombineCABundleConfigMaps(destinationConfigMap ResourceLocation, lister cor } return cm, nil } + +func CombineCABundleConfigMapsOptimistically(destinationConfigMap *corev1.ConfigMap, lister corev1listers.ConfigMapLister, additionalAnnotations certrotation.AdditionalAnnotations, inputConfigMaps ...ResourceLocation) (*corev1.ConfigMap, bool, error) { + var cm *corev1.ConfigMap + if destinationConfigMap == nil { + cm = &corev1.ConfigMap{} + } else { + cm = destinationConfigMap.DeepCopy() + } + certificates := []*x509.Certificate{} + for _, input := range inputConfigMaps { + inputConfigMap, err := lister.ConfigMaps(input.Namespace).Get(input.Name) + if apierrors.IsNotFound(err) { + continue + } + if err != nil { + return nil, false, err + } + + // configmaps must conform to this + inputContent := inputConfigMap.Data["ca-bundle.crt"] + if len(inputContent) == 0 { + continue + } + inputCerts, err := cert.ParseCertsPEM([]byte(inputContent)) + if err != nil { + return nil, false, fmt.Errorf("configmap/%s in %q is malformed: %v", input.Name, input.Namespace, err) + } + certificates = append(certificates, inputCerts...) + } + + certificates = crypto.FilterExpiredCerts(certificates...) + finalCertificates := []*x509.Certificate{} + // now check for duplicates. n^2, but super simple + for i := range certificates { + found := false + for j := range finalCertificates { + if reflect.DeepEqual(certificates[i].Raw, finalCertificates[j].Raw) { + found = true + break + } + } + if !found { + finalCertificates = append(finalCertificates, certificates[i]) + } + } + + caBytes, err := crypto.EncodeCertificates(finalCertificates...) + if err != nil { + return nil, false, err + } + + modified := additionalAnnotations.EnsureTLSMetadataUpdate(&cm.ObjectMeta) + newCMData := map[string]string{ + "ca-bundle.crt": string(caBytes), + } + if !reflect.DeepEqual(cm.Data, newCMData) { + cm.Data = newCMData + modified = true + } + return cm, modified, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5abd7824f2..1906b47f92 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -349,7 +349,7 @@ github.com/openshift/client-go/security/informers/externalversions/internalinter github.com/openshift/client-go/security/informers/externalversions/security github.com/openshift/client-go/security/informers/externalversions/security/v1 github.com/openshift/client-go/security/listers/security/v1 -# github.com/openshift/library-go v0.0.0-20250512121900-863508cf7a27 +# github.com/openshift/library-go v0.0.0-20250512121900-863508cf7a27 => github.com/openshift-cherrypick-robot/library-go v0.0.0-20250912134350-65142f98d552 ## explicit; go 1.23.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/assets @@ -1556,3 +1556,4 @@ sigs.k8s.io/structured-merge-diff/v4/value ## explicit; go 1.12 sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 +# github.com/openshift/library-go => github.com/openshift-cherrypick-robot/library-go v0.0.0-20250912134350-65142f98d552