From 8de0005d17b29a18fbd7e015f49d827fd0be4706 Mon Sep 17 00:00:00 2001 From: Haseeb Tariq Date: Thu, 9 Jul 2026 12:58:54 -0700 Subject: [PATCH] render: Resolve featureSet into individual feature gates during bootstrap The render path's getFeatureGatesStatus only parsed explicit featureGates key=value entries from the install-config. It did not resolve featureSet (e.g. TechPreviewNoUpgrade) into the constituent gate list. When a cluster is installed with featureSet: TechPreviewNoUpgrade and no explicit featureGates entries, the ConfigurablePKI gate was never detected, causing bootstrap etcd-signer certs to be generated with legacy RSA-2048 instead of the configured algorithm. Use features.FeatureSets() from openshift/api to resolve the featureSet into enabled/disabled gates, then layer explicit featureGates overrides on top for CustomNoUpgrade support. Assisted-by: Claude Code (Opus 4.6) --- pkg/cmd/render/render.go | 40 ++++++--- pkg/cmd/render/render_test.go | 156 ++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/render/render.go b/pkg/cmd/render/render.go index 06b8c9d9b..1f37bd860 100644 --- a/pkg/cmd/render/render.go +++ b/pkg/cmd/render/render.go @@ -803,20 +803,32 @@ func getFeatureGatesStatus(installConfig map[string]any) (sets.Set[configv1.Feat necessaryFeatureGates := []configv1.FeatureGateName{"ShortCertRotation"} disabled.Insert(necessaryFeatureGates...) - featureGates, found := installConfig["featureGates"] - if !found { - return enabled, disabled - } - - for _, featureGate := range featureGates.([]any) { - key := strings.Split(featureGate.(string), "=")[0] - value := strings.Split(featureGate.(string), "=")[1] - if value == "true" { - enabled = enabled.Insert(configv1.FeatureGateName(key)) - disabled = disabled.Delete(configv1.FeatureGateName(key)) - } else if value == "false" { - enabled = enabled.Delete(configv1.FeatureGateName(key)) - disabled = disabled.Insert(configv1.FeatureGateName(key)) + if featureSetRaw, found := installConfig["featureSet"]; found { + featureSet := configv1.FeatureSet(featureSetRaw.(string)) + if resolved := features.FeatureSets(0, features.SelfManaged, featureSet); resolved != nil { + for _, fg := range resolved.Enabled { + enabled.Insert(fg.FeatureGateAttributes.Name) + disabled.Delete(fg.FeatureGateAttributes.Name) + } + for _, fg := range resolved.Disabled { + if !enabled.Has(fg.FeatureGateAttributes.Name) { + disabled.Insert(fg.FeatureGateAttributes.Name) + } + } + } + } + + if featureGates, found := installConfig["featureGates"]; found { + for _, featureGate := range featureGates.([]any) { + key := strings.Split(featureGate.(string), "=")[0] + value := strings.Split(featureGate.(string), "=")[1] + if value == "true" { + enabled = enabled.Insert(configv1.FeatureGateName(key)) + disabled = disabled.Delete(configv1.FeatureGateName(key)) + } else if value == "false" { + enabled = enabled.Delete(configv1.FeatureGateName(key)) + disabled = disabled.Insert(configv1.FeatureGateName(key)) + } } } diff --git a/pkg/cmd/render/render_test.go b/pkg/cmd/render/render_test.go index e7cc386a7..853507e82 100644 --- a/pkg/cmd/render/render_test.go +++ b/pkg/cmd/render/render_test.go @@ -349,6 +349,64 @@ data: ecdsa: curve: P521 ` + + clusterConfigMapWithCustomPKIAndFeatureSet = ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cluster-config-v1 + namespace: kube-system +data: + install-config: | + apiVersion: v1 + baseDomain: gcp.devcluster.openshift.com + compute: + - architecture: amd64 + hyperthreading: Enabled + name: worker + platform: {} + replicas: 3 + controlPlane: + architecture: amd64 + hyperthreading: Enabled + name: master + platform: + gcp: + osDisk: + DiskSizeGB: 128 + DiskType: pd-ssd + type: n1-standard-4 + zones: + - us-east1-b + - us-east1-c + - us-east1-d + replicas: 3 + metadata: + creationTimestamp: null + name: my-cluster + networking: + clusterNetwork: + - cidr: 10.128.0.0/14 + hostPrefix: 23 + machineCIDR: 10.0.0.0/16 + machineNetwork: + - cidr: 10.0.0.0/16 + networkType: OpenShiftSDN + serviceNetwork: + - 172.30.0.0/16 + platform: + gcp: + projectID: openshift + region: us-east1 + publish: External + featureSet: TechPreviewNoUpgrade + pki: + signerCertificates: + key: + algorithm: ECDSA + ecdsa: + curve: P521 +` ) type testConfig struct { @@ -607,6 +665,53 @@ func TestTemplateDataWithCustomPKI(t *testing.T) { testTemplateData(t, config, validateECDSAP521Signer) } +func TestTemplateDataWithCustomPKIFeatureSet(t *testing.T) { + validateECDSAP521Signer := func(t *testing.T, td *TemplateData) { + if len(td.certificates) == 0 { + t.Fatal("no certificates generated") + } + + var signerCert []byte + for _, cert := range td.certificates { + if cert.Name == "etcd-signer" { + signerCert = cert.Data["tls.crt"] + break + } + } + + if signerCert == nil { + t.Fatal("etcd-signer certificate not found in generated certificates") + } + + block, _ := pem.Decode(signerCert) + if block == nil { + t.Fatal("failed to decode PEM block from etcd-signer certificate") + } + + x509Cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse x509 certificate: %v", err) + } + + ecdsaKey, ok := x509Cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + t.Fatalf("expected ECDSA public key, got %T", x509Cert.PublicKey) + } + + if ecdsaKey.Curve != elliptic.P521() { + t.Errorf("expected P521 curve, got %v", ecdsaKey.Curve.Params().Name) + } + } + + config := &testConfig{ + clusterNetworkConfig: networkConfigIpv4, + infraConfig: infraConfig, + clusterConfigMap: clusterConfigMapWithCustomPKIAndFeatureSet, + } + + testTemplateData(t, config, validateECDSAP521Signer) +} + func hasClusterCIDR(expected ...string) func(*testing.T, *TemplateData) { return func(t *testing.T, td *TemplateData) { if len(td.ClusterCIDR) != len(expected) { @@ -1020,6 +1125,27 @@ featureGates: [ShortCertRotation=true, UpgradeStatus=foobar] expectedEnabled: sets.New(features.FeatureShortCertRotation), expectedDisabled: sets.New[configv1.FeatureGateName](), }, + "featureSet TechPreviewNoUpgrade": { + installConfig: ` +apiVersion: v1 +metadata: + name: my-cluster +featureSet: TechPreviewNoUpgrade +`, + expectedEnabled: techPreviewEnabledGates(), + expectedDisabled: techPreviewDisabledGates(), + }, + "featureSet with featureGates override": { + installConfig: ` +apiVersion: v1 +metadata: + name: my-cluster +featureSet: TechPreviewNoUpgrade +featureGates: [ConfigurablePKI=false] +`, + expectedEnabled: techPreviewEnabledGatesWithout(features.FeatureGateConfigurablePKI), + expectedDisabled: techPreviewDisabledGatesWith(features.FeatureGateConfigurablePKI), + }, } for name, test := range tests { @@ -1197,3 +1323,33 @@ pki: }) } } + +func techPreviewEnabledGates() sets.Set[configv1.FeatureGateName] { + resolved := features.FeatureSets(0, features.SelfManaged, configv1.TechPreviewNoUpgrade) + s := sets.New[configv1.FeatureGateName]() + for _, fg := range resolved.Enabled { + s.Insert(fg.FeatureGateAttributes.Name) + } + return s +} + +func techPreviewDisabledGates() sets.Set[configv1.FeatureGateName] { + resolved := features.FeatureSets(0, features.SelfManaged, configv1.TechPreviewNoUpgrade) + s := sets.New[configv1.FeatureGateName]() + for _, fg := range resolved.Disabled { + s.Insert(fg.FeatureGateAttributes.Name) + } + return s +} + +func techPreviewEnabledGatesWithout(gates ...configv1.FeatureGateName) sets.Set[configv1.FeatureGateName] { + s := techPreviewEnabledGates() + s.Delete(gates...) + return s +} + +func techPreviewDisabledGatesWith(gates ...configv1.FeatureGateName) sets.Set[configv1.FeatureGateName] { + s := techPreviewDisabledGates() + s.Insert(gates...) + return s +}