Skip to content

Commit 0431e18

Browse files
Merge pull request #229 from swghosh/cm-424
CM-424: Allow experimental features to be turned on from `--unsupported-addon-features`
2 parents 54942c4 + e7d67b7 commit 0431e18

File tree

10 files changed

+202
-19
lines changed

10 files changed

+202
-19
lines changed

api/operator/v1alpha1/features.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package v1alpha1
2+
3+
import (
4+
"k8s.io/component-base/featuregate"
5+
)
6+
7+
var (
8+
// TechPreview: v1.15
9+
//
10+
// IstioCSR enables the controller for istiocsr.operator.openshift.io resource,
11+
// which extends cert-manager-operator to deploy and manage the istio-csr agent.
12+
// OpenShift Service Mesh facilitates the integration and istio-csr is an agent that
13+
// allows Istio workload and control plane components to be secured using cert-manager.
14+
//
15+
// For more details,
16+
// https://github.com/openshift/enhancements/blob/master/enhancements/cert-manager/istio-csr-controller.md
17+
FeatureIstioCSR featuregate.Feature = "IstioCSR"
18+
)
19+
20+
var OperatorFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
21+
FeatureIstioCSR: {Default: false, PreRelease: "TechPreview"},
22+
}

bundle/manifests/cert-manager-operator.clusterserviceversion.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ spec:
673673
- --v=$(OPERATOR_LOG_LEVEL)
674674
- --trusted-ca-configmap=$(TRUSTED_CA_CONFIGMAP_NAME)
675675
- --cloud-credentials-secret=$(CLOUD_CREDENTIALS_SECRET_NAME)
676+
- --unsupported-addon-features=$(UNSUPPORTED_ADDON_FEATURES)
676677
command:
677678
- /usr/bin/cert-manager-operator
678679
env:
@@ -706,6 +707,7 @@ spec:
706707
value: "2"
707708
- name: TRUSTED_CA_CONFIGMAP_NAME
708709
- name: CLOUD_CREDENTIALS_SECRET_NAME
710+
- name: UNSUPPORTED_ADDON_FEATURES
709711
image: openshift.io/cert-manager-operator:latest
710712
imagePullPolicy: IfNotPresent
711713
name: cert-manager-operator

config/manager/manager.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ spec:
6262
- '--v=$(OPERATOR_LOG_LEVEL)'
6363
- '--trusted-ca-configmap=$(TRUSTED_CA_CONFIGMAP_NAME)'
6464
- '--cloud-credentials-secret=$(CLOUD_CREDENTIALS_SECRET_NAME)'
65+
- '--unsupported-addon-features=$(UNSUPPORTED_ADDON_FEATURES)'
6566
env:
6667
- name: WATCH_NAMESPACE
6768
valueFrom:
@@ -93,6 +94,7 @@ spec:
9394
value: '2'
9495
- name: TRUSTED_CA_CONFIGMAP_NAME
9596
- name: CLOUD_CREDENTIALS_SECRET_NAME
97+
- name: UNSUPPORTED_ADDON_FEATURES
9698
image: controller:latest
9799
imagePullPolicy: IfNotPresent
98100
name: cert-manager-operator

pkg/cmd/operator/cmd.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,16 @@ func NewOperator() *cobra.Command {
2020
cmd.Short = "Start the cert-manager Operator"
2121
cmd.Flags().StringVar(&operator.TrustedCAConfigMapName, "trusted-ca-configmap", "", "The name of the config map containing TLS CA(s) which should be trusted by the controller's containers. PEM encoded file under \"ca-bundle.crt\" key is expected.")
2222
cmd.Flags().StringVar(&operator.CloudCredentialSecret, "cloud-credentials-secret", "", "The name of the secret containing cloud credentials for authenticating using cert-manager ambient credentials mode.")
23+
24+
cmd.Flags().StringVar(&operator.UnsupportedAddonFeatures, "unsupported-addon-features", "",
25+
`List of unsupported addon features that the operator optionally enables.
26+
27+
eg. --unsupported-addon-features="IstioCSR=true"
28+
29+
Note: Technology Preview features are not supported with Red Hat production service level agreements (SLAs)
30+
and might not be functionally complete. Red Hat does not recommend using them in production.
31+
32+
These features provide early access to upcoming product features,
33+
enabling customers to test functionality and provide feedback during the development process.`)
2334
return cmd
2435
}

pkg/features/features.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package features
2+
3+
import (
4+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
5+
"k8s.io/component-base/featuregate"
6+
7+
"github.com/openshift/cert-manager-operator/api/operator/v1alpha1"
8+
)
9+
10+
var (
11+
// mutableFeatureGate is the top-level FeatureGate with mutability (read/write).
12+
mutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()
13+
14+
// DefaultFeatureGate is the top-level shared global FeatureGate (read-only).
15+
DefaultFeatureGate featuregate.FeatureGate = mutableFeatureGate
16+
)
17+
18+
func init() {
19+
utilruntime.Must(mutableFeatureGate.Add(v1alpha1.OperatorFeatureGates))
20+
}
21+
22+
func SetupWithFlagValue(flagValue string) error {
23+
return mutableFeatureGate.Set(flagValue)
24+
}

pkg/features/features_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package features
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"k8s.io/component-base/featuregate"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
// expectedDefaultFeatureState is a map of features with known states
14+
//
15+
// Always update this test map when adding a new FeatureName to the api,
16+
// a test is added below to enforce this.
17+
var expectedDefaultFeatureState = map[bool][]featuregate.Feature{
18+
// features ENABLED by default,
19+
// list of features which are expected to be enabled at runtime.
20+
true: {},
21+
22+
// features DISABLED by default,
23+
// list of features which are expected to be disabled at runtime.
24+
false: {
25+
featuregate.Feature("IstioCSR"),
26+
},
27+
}
28+
29+
func TestFeatureGates(t *testing.T) {
30+
t.Run("runtime features to test should be identical with operator features", func(t *testing.T) {
31+
testFeatureNames := make([]featuregate.Feature, 0)
32+
for _, featureNames := range expectedDefaultFeatureState {
33+
testFeatureNames = append(testFeatureNames, featureNames...)
34+
}
35+
36+
knownOperatorFeatures := make([]featuregate.Feature, 0)
37+
feats := mutableFeatureGate.GetAll()
38+
for feat := range feats {
39+
// skip "AllBeta", "AllAlpha": our operator does not use those
40+
if feat == "AllBeta" || feat == "AllAlpha" {
41+
continue
42+
}
43+
knownOperatorFeatures = append(knownOperatorFeatures, feat)
44+
}
45+
46+
assert.Equal(t, knownOperatorFeatures, testFeatureNames,
47+
`the list of features known to the operator differ from what is being tested here,
48+
it could be that there was a new Feature added to the api which wasn't added to the tests.
49+
Please verify "api/operator/v1alpha1" and "pkg/operator/features" have identical features.`)
50+
})
51+
52+
t.Run("all runtime features should honor expected default feature state", func(t *testing.T) {
53+
for expectedDefaultState, features := range expectedDefaultFeatureState {
54+
for _, featureName := range features {
55+
assert.Equal(t, expectedDefaultState, DefaultFeatureGate.Enabled(featureName),
56+
"violated by feature=%s", featureName)
57+
}
58+
}
59+
})
60+
61+
t.Run("all TechPreview features should be disabled by default", func(t *testing.T) {
62+
feats := mutableFeatureGate.GetAll()
63+
for feat, spec := range feats {
64+
// skip "AllBeta", "AllAlpha": our operator does not use those
65+
if feat == "AllBeta" || feat == "AllAlpha" {
66+
continue
67+
}
68+
69+
assert.Equal(t, spec.PreRelease == "TechPreview", !spec.Default,
70+
"prerelease TechPreview %q feature should default to disabled",
71+
feat)
72+
}
73+
})
74+
75+
t.Run("runtime features should allow enabling all feature gates that are disabled by default", func(t *testing.T) {
76+
// enable all disabled features
77+
defaultDisabledFeatures := expectedDefaultFeatureState[false]
78+
79+
featVals := make([]string, len(defaultDisabledFeatures))
80+
for i := range featVals {
81+
featVals[i] = fmt.Sprintf("%s=true", defaultDisabledFeatures[i])
82+
}
83+
84+
err := mutableFeatureGate.Set(strings.Join(featVals, ","))
85+
assert.NoError(t, err)
86+
87+
// check if all those features were enabled successfully
88+
for _, featureName := range defaultDisabledFeatures {
89+
assert.Equal(t, true, DefaultFeatureGate.Enabled(featureName),
90+
"violated by feature=%s", featureName)
91+
}
92+
})
93+
}

pkg/operator/starter.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import (
1717
"github.com/openshift/library-go/pkg/operator/status"
1818
"github.com/openshift/library-go/pkg/operator/v1helpers"
1919

20+
"github.com/openshift/cert-manager-operator/api/operator/v1alpha1"
2021
"github.com/openshift/cert-manager-operator/pkg/controller/deployment"
22+
"github.com/openshift/cert-manager-operator/pkg/features"
2123
certmanoperatorclient "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned"
2224
certmanoperatorinformers "github.com/openshift/cert-manager-operator/pkg/operator/informers/externalversions"
2325
"github.com/openshift/cert-manager-operator/pkg/operator/operatorclient"
@@ -35,6 +37,10 @@ var TrustedCAConfigMapName string
3537
// used in ambient credentials mode, and is provided as a runtime arg
3638
var CloudCredentialSecret string
3739

40+
// UnsupportedAddonFeatures is the user-specific list of unsupported addon features
41+
// that the operator optionally enables, and is provided as a runtime arg.
42+
var UnsupportedAddonFeatures string
43+
3844
func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error {
3945
kubeClient, err := kubernetes.NewForConfig(cc.ProtoKubeConfig)
4046
if err != nil {
@@ -112,12 +118,21 @@ func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
112118
go controller.Run(ctx, 1)
113119
}
114120

115-
manager, err := NewControllerManager()
121+
err = features.SetupWithFlagValue(UnsupportedAddonFeatures)
116122
if err != nil {
117-
return fmt.Errorf("failed to create controller manager: %w", err)
123+
return fmt.Errorf("failed to parse addon features: %w", err)
118124
}
119-
if err := manager.Start(ctrl.SetupSignalHandler()); err != nil {
120-
return fmt.Errorf("failed to start istiocsr controller: %w", err)
125+
126+
// enable controller-runtime and istio-csr controller
127+
// only when "IstioCSR" feature is turned on from --addon-features
128+
if features.DefaultFeatureGate.Enabled(v1alpha1.FeatureIstioCSR) {
129+
manager, err := NewControllerManager()
130+
if err != nil {
131+
return fmt.Errorf("failed to create controller manager: %w", err)
132+
}
133+
if err := manager.Start(ctrl.SetupSignalHandler()); err != nil {
134+
return fmt.Errorf("failed to start istiocsr controller: %w", err)
135+
}
121136
}
122137

123138
<-ctx.Done()

test/e2e/certificates_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ var _ = Describe("ACME Certificate", Ordered, func() {
205205
Expect(err).NotTo(HaveOccurred())
206206

207207
By("setting cloud credential secret name in subscription object")
208-
credentialSecret := "aws-creds"
209-
err = patchSubscriptionWithCloudCredential(ctx, loader, credentialSecret)
208+
err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{
209+
"CLOUD_CREDENTIALS_SECRET_NAME": "aws-creds",
210+
})
210211
Expect(err).NotTo(HaveOccurred())
211212

212213
By("getting AWS zone from Infrastructure object")
@@ -297,8 +298,9 @@ var _ = Describe("ACME Certificate", Ordered, func() {
297298
Expect(err).NotTo(HaveOccurred())
298299

299300
By("setting cloud credential secret name in subscription object")
300-
credentialSecret := "aws-creds"
301-
err = patchSubscriptionWithCloudCredential(ctx, loader, credentialSecret)
301+
err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{
302+
"CLOUD_CREDENTIALS_SECRET_NAME": "aws-creds",
303+
})
302304
Expect(err).NotTo(HaveOccurred())
303305

304306
By("getting AWS zone from Infrastructure object")
@@ -496,7 +498,9 @@ var _ = Describe("ACME Certificate", Ordered, func() {
496498
Expect(err).NotTo(HaveOccurred())
497499

498500
By("Configure cert-manager to use credential, setting this credential secret name in subscription object")
499-
err = patchSubscriptionWithCloudCredential(ctx, loader, credentialSecret)
501+
err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{
502+
"CLOUD_CREDENTIALS_SECRET_NAME": credentialSecret,
503+
})
500504
Expect(err).NotTo(HaveOccurred())
501505

502506
By("Getting GCP project ID from Infrastructure object")

test/e2e/istio_csr_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ var _ = Describe("Istio-CSR", Ordered, Label("TechPreview", "Feature:IstioCSR"),
4646

4747
dynamicClient, err = dynamic.NewForConfig(cfg)
4848
Expect(err).Should(BeNil())
49+
50+
By("enable IstioCSR addon feature by patching subscription object")
51+
err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{
52+
"UNSUPPORTED_ADDON_FEATURES": "IstioCSR=true",
53+
})
54+
Expect(err).NotTo(HaveOccurred())
4955
})
5056

5157
var ns *corev1.Namespace

test/e2e/utils_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -400,24 +400,28 @@ func getCertManagerOperatorSubscription(ctx context.Context, loader library.Dyna
400400
return subName, nil
401401
}
402402

403-
// patchSubscriptionWithCloudCredential uses the k8s dynamic client to patche the only Subscription object
404-
// in the cert-manager-operator namespace to inject CLOUD_CREDENTIALS_SECRET_NAME="aws-creds" env
405-
// into its spec.config.env
406-
func patchSubscriptionWithCloudCredential(ctx context.Context, loader library.DynamicResourceLoader, credentialSecret string) error {
403+
// patchSubscriptionWithEnvVars uses the k8s dynamic client to patch the only Subscription object
404+
// in the cert-manager-operator namespace, inject specified env vars into spec.config.env
405+
func patchSubscriptionWithEnvVars(ctx context.Context, loader library.DynamicResourceLoader, envVars map[string]string) error {
407406
subName, err := getCertManagerOperatorSubscription(ctx, loader)
408407
if err != nil {
409408
return err
410409
}
411410

411+
env := make([]interface{}, len(envVars))
412+
i := 0
413+
for k, v := range envVars {
414+
env[i] = map[string]interface{}{
415+
"name": k,
416+
"value": v,
417+
}
418+
i++
419+
}
420+
412421
patch := map[string]interface{}{
413422
"spec": map[string]interface{}{
414423
"config": map[string]interface{}{
415-
"env": []interface{}{
416-
map[string]interface{}{
417-
"name": "CLOUD_CREDENTIALS_SECRET_NAME",
418-
"value": credentialSecret,
419-
},
420-
},
424+
"env": env,
421425
},
422426
},
423427
}

0 commit comments

Comments
 (0)