Skip to content

Commit 1704403

Browse files
authored
Automatically enable feature gates
Automatically enables feature gates in the CSI sidecars during installation.
1 parent 2fcdb2c commit 1704403

File tree

7 files changed

+502
-2
lines changed

7 files changed

+502
-2
lines changed

cli/cmd/install.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,14 @@ func installTrident() (returnError error) {
865865
}
866866
}
867867

868+
// Automatically enable CSI feature gates.
869+
csiFeatureGateYAMLSnippets, err := k8sclient.ConstructCSIFeatureGateYAMLSnippets(client)
870+
if err != nil {
871+
Log().WithError(err).Debug("Could not enable some CSI feature gates.")
872+
} else {
873+
Log().WithField("featureGates", csiFeatureGateYAMLSnippets).Debug("Enabling CSI feature gates.")
874+
}
875+
868876
// All checks succeeded, so proceed with installation
869877
Log().WithField("namespace", TridentPodNamespace).Info("Starting Trident installation.")
870878

@@ -1058,6 +1066,7 @@ func installTrident() (returnError error) {
10581066
IdentityLabel: identityLabel,
10591067
K8sAPIQPS: k8sAPIQPS,
10601068
EnableConcurrency: enableConcurrency,
1069+
CSIFeatureGates: csiFeatureGateYAMLSnippets,
10611070
}
10621071
returnError = client.CreateObjectByYAML(
10631072
k8sclient.GetCSIDeploymentYAML(deploymentArgs))

cli/k8s_client/feature_gate.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright 2025 NetApp, Inc. All Rights Reserved.
2+
3+
package k8sclient
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/netapp/trident/pkg/collection"
11+
)
12+
13+
const (
14+
apiV1Alpha1 = "v1alpha1"
15+
apiV1Beta1 = "v1beta1"
16+
apiV1Beta2 = "v1beta2"
17+
apiV1 = "v1"
18+
19+
// AutoFeatureGateVolumeGroupSnapshot is the feature gate template string for group snapshots.
20+
AutoFeatureGateVolumeGroupSnapshot = "VolumeGroupSnapshot"
21+
22+
// volumeGroupSnapshotCRDName is a constant for the VolumeGroupSnapshot CRD name. This CRD may or may not be
23+
// installed. If it is present, Trident should enable the VolumeGroupSnapshot feature.
24+
volumeGroupSnapshotCRDName = "volumegroupsnapshots.groupsnapshot.storage.k8s.io"
25+
volumeGroupSnapshotClassCRDName = "volumegroupsnapshotclasses.groupsnapshot.storage.k8s.io"
26+
volumeGroupSnapshotContentCRDName = "volumegroupsnapshotcontents.groupsnapshot.storage.k8s.io"
27+
)
28+
29+
var (
30+
// autoFeatureGates is a list of known feature gates that are automatically enabled.
31+
// These MUST be distinct from the typical feature gates that are set in the Trident configuration file.
32+
autoFeatureGates []autoFeatureGate
33+
34+
// autoFeatureGateVolumeGroupSnapshot is the auto-enabling or (disabling) feature gate for VolumeGroupSnapshot.
35+
autoFeatureGateVolumeGroupSnapshot = autoFeatureGate{
36+
name: AutoFeatureGateVolumeGroupSnapshot,
37+
crds: []string{
38+
volumeGroupSnapshotCRDName,
39+
volumeGroupSnapshotClassCRDName,
40+
volumeGroupSnapshotContentCRDName,
41+
},
42+
supportedAPIs: []string{
43+
apiV1Beta1,
44+
apiV1Beta2,
45+
apiV1,
46+
},
47+
gates: map[string]string{
48+
// The placeholder key is used to inject the feature gate into the Trident configuration file.
49+
// It MUST match the placeholder key in the YAML.
50+
// Placeholders can be duplicated across multiple feature gates, but the values must be unique.
51+
"{FEATURE_GATES_CSI_SNAPSHOTTER}": "CSIVolumeGroupSnapshot=true",
52+
},
53+
}
54+
)
55+
56+
func init() {
57+
autoFeatureGates = []autoFeatureGate{
58+
autoFeatureGateVolumeGroupSnapshot,
59+
}
60+
}
61+
62+
// autoFeatureGate represents the link between a feature, its requirements and YAML placeholders to gates
63+
// that may be replaced in the Trident YAML files.
64+
type autoFeatureGate struct {
65+
name string // name of the feature that's being gated.
66+
crds []string // CRDs that must be present for the feature to be enabled. If no CRDs are required, leave empty.
67+
supportedAPIs []string // API versions that must be present. If no supportedAPIs are required, leave empty.
68+
gates map[string]string // YAML gates that may be replaced in the Trident configuration file. Always required.
69+
}
70+
71+
func (g autoFeatureGate) GoString() string {
72+
return fmt.Sprintf("name: %s, requiredCRDs: %v, supportedAPIs: %v", g.name, g.crds, g.supportedAPIs)
73+
}
74+
75+
func (g autoFeatureGate) String() string {
76+
return fmt.Sprintf("name: %s, requiredCRDs: %v, supportedAPIs: %v", g.name, g.crds, g.supportedAPIs)
77+
}
78+
79+
func (g autoFeatureGate) Gates() map[string]string {
80+
snippets := make(map[string]string, len(g.gates))
81+
for k, v := range g.gates {
82+
snippets[k] = v
83+
}
84+
return snippets
85+
}
86+
87+
// ConstructCSIFeatureGateYAMLSnippets looks at all predefined feature gates and builds a list of
88+
// YAML snippets for the Trident Deployment and DaemonSet.
89+
// If any of the feature gates are not safe to enable, it will gather errors and log a warning.
90+
// Importantly, this is not an all or nothing operation.
91+
// If some but not all feature gates are safe to enable, they will be returned along with an error.
92+
func ConstructCSIFeatureGateYAMLSnippets(client KubernetesClient) (map[string]string, error) {
93+
if client == nil {
94+
return nil, fmt.Errorf("k8s API client is nil; cannot auto-enable feature gates")
95+
}
96+
97+
// Automatically enable feature gates.
98+
// A given placeholder key can have 1-many feature gates associated with it.
99+
// Example:
100+
// feature gate arg: `- "--feature-gates=CSIVolumeGroupSnapshot=true,CSIAnotherFeature=true"`
101+
// This means that the `CSIVolumeGroupSnapshot` and `CSIAnotherFeature` feature gates
102+
// are both enabled, and the `--feature-gates` argument will be both be injected and must be squashed.
103+
// If there are multiple feature gates that need to be injected, they will be squashed into a single
104+
// `--feature-gates` argument.
105+
var errs error
106+
placeholderToGates := make(map[string]map[string]struct{}) // snippets to inject for feature gates
107+
for _, autoFeatureGate := range autoFeatureGates {
108+
canEnable, err := canAutoEnableFeatureGate(client, autoFeatureGate)
109+
if err != nil {
110+
errs = errors.Join(errs, err)
111+
continue
112+
} else if !canEnable {
113+
errs = errors.Join(errs, fmt.Errorf("feature gate '%s' cannot be enabled", autoFeatureGate.name))
114+
continue
115+
}
116+
117+
for placeholder, featureGate := range autoFeatureGate.Gates() {
118+
gates, exists := placeholderToGates[placeholder]
119+
if !exists {
120+
gates = make(map[string]struct{})
121+
}
122+
// Some feature gates may already be specified as a comma-separated list, so we need to split them
123+
// and join them later with others that share the same placeholder.
124+
for _, gate := range strings.Split(featureGate, ",") {
125+
gate = strings.TrimSpace(gate)
126+
if gate != "" {
127+
gates[gate] = struct{}{}
128+
}
129+
}
130+
placeholderToGates[placeholder] = gates
131+
}
132+
}
133+
134+
// Now we have a map of placeholders to feature gates. We need to build the snippets. Feature gates sharing the
135+
// same placeholder will be squashed into a single string delimited by commas.
136+
snippets := make(map[string]string, len(placeholderToGates))
137+
for placeholder, gates := range placeholderToGates {
138+
if len(gates) == 0 {
139+
continue // No gates for this placeholder, skip it.
140+
}
141+
142+
// Join the gates with commas and add to the snippets map.
143+
gateList := make([]string, 0, len(gates))
144+
for gate := range gates {
145+
gateList = append(gateList, gate)
146+
}
147+
snippets[placeholder] = strings.Join(gateList, ",")
148+
}
149+
150+
return snippets, errs
151+
}
152+
153+
// canAutoEnableFeatureGate checks if all autoFeatureGate requirements are met, such as the presence of
154+
// CRDs and correct API versions. If any precondition is not met, it returns an error indicating which
155+
// requirements are not satisfied and does not allow Trident to automatically enable the feature gate.
156+
func canAutoEnableFeatureGate(client KubernetesClient, gate autoFeatureGate) (bool, error) {
157+
var errs error
158+
for _, crdName := range gate.crds {
159+
crdExists, err := client.CheckCRDExists(crdName)
160+
if err != nil {
161+
// This will only fail if the error is not a "not found" error. Not found is OK
162+
errs = errors.Join(errs, err)
163+
continue
164+
} else if !crdExists {
165+
// If we get here, the CRD is missing.
166+
err = fmt.Errorf("CRD '%s' is missing for feature '%s'", crdName, gate.name)
167+
errs = errors.Join(errs, err)
168+
continue
169+
}
170+
171+
// Now we know this CRD exists. Validate the API versions.
172+
crd, err := client.GetCRD(crdName)
173+
if err != nil || crd == nil {
174+
err = fmt.Errorf("failed to get CRD '%s' for feature '%s'; %w", crdName, gate.name, err)
175+
errs = errors.Join(errs, err)
176+
continue
177+
}
178+
179+
// Check if the discovered CRD version is one of the allowed versions.
180+
// If the CRD API Version is not at least one of the supported versions, we cannot enable the feature.
181+
if len(gate.supportedAPIs) > 0 {
182+
// We need to check both Spec.Versions and Status.StoredVersions.
183+
found := false
184+
for _, version := range crd.Spec.Versions {
185+
if collection.ContainsString(gate.supportedAPIs, version.Name) {
186+
found = true
187+
break
188+
}
189+
}
190+
191+
if !found {
192+
// If we still do not have a matching version, we cannot enable the feature.
193+
err = fmt.Errorf("CRD '%s' does not have a supported API versions for feature '%s'", crdName, gate.name)
194+
errs = errors.Join(errs, err)
195+
}
196+
}
197+
}
198+
199+
// If any CRDs are missing, do not attempt to enable the feature.
200+
if errs != nil {
201+
return false, errs
202+
}
203+
204+
return true, nil
205+
}

0 commit comments

Comments
 (0)