@@ -3,12 +3,49 @@ package common
33import (
44 "context"
55 "fmt"
6+ "regexp"
7+ "strings"
68
79 configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
10+ v1 "k8s.io/api/core/v1"
811 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+ clientset "k8s.io/client-go/kubernetes"
913 "k8s.io/kubernetes/test/e2e/framework"
1014)
1115
16+ const (
17+ cloudConfigNamespace = "openshift-cloud-controller-manager"
18+ cloudConfigName = "cloud-conf"
19+ )
20+
21+ // GetOcClient returns an OpenShift config/v1 API client (FeatureGates, Infrastructures, etc.).
22+ func GetOcClient (ctx context.Context ) (* configv1client.ConfigV1Client , error ) {
23+ restConfig , err := framework .LoadConfig ()
24+ if err != nil {
25+ return nil , fmt .Errorf ("failed to load kubeconfig: %w" , err )
26+ }
27+ configClient , err := configv1client .NewForConfig (restConfig )
28+ if err != nil {
29+ return nil , fmt .Errorf ("failed to openshift client: %w" , err )
30+ }
31+
32+ return configClient , nil
33+ }
34+
35+ // GetKubeClient returns a core Kubernetes client (Pods, ConfigMaps, Services, etc.).
36+ func GetKubeClient (ctx context.Context ) (clientset.Interface , error ) {
37+ restConfig , err := framework .LoadConfig ()
38+ if err != nil {
39+ return nil , fmt .Errorf ("failed to load kubeconfig: %w" , err )
40+ }
41+ cs , err := clientset .NewForConfig (restConfig )
42+ if err != nil {
43+ return nil , fmt .Errorf ("failed to kube clientset: %w" , err )
44+ }
45+
46+ return cs , nil
47+ }
48+
1249// IsFeatureEnabled checks if an OpenShift feature gate is enabled by querying the
1350// FeatureGate resource named "cluster" using the typed OpenShift config API.
1451//
@@ -27,22 +64,16 @@ import (
2764// Note: For HyperShift clusters, this checks the management cluster's feature gates.
2865// To check hosted cluster feature gates, use the hosted cluster's kubeconfig.
2966func IsFeatureEnabled (ctx context.Context , featureName string ) (bool , error ) {
30- // Get the REST config
31- restConfig , err := framework .LoadConfig ()
32- if err != nil {
33- return false , fmt .Errorf ("failed to load kubeconfig: %v" , err )
34- }
35-
3667 // Create typed config client (more efficient than dynamic client)
37- configClient , err := configv1client . NewForConfig ( restConfig )
68+ oclient , err := GetOcClient ( ctx )
3869 if err != nil {
39- return false , fmt .Errorf ("failed to create config client: %v " , err )
70+ return false , fmt .Errorf ("failed to create config client: %w " , err )
4071 }
4172
4273 // Get the FeatureGate resource using typed API
43- featureGate , err := configClient .FeatureGates ().Get (ctx , "cluster" , metav1.GetOptions {})
74+ featureGate , err := oclient .FeatureGates ().Get (ctx , "cluster" , metav1.GetOptions {})
4475 if err != nil {
45- return false , fmt .Errorf ("failed to get FeatureGate 'cluster': %v " , err )
76+ return false , fmt .Errorf ("failed to get FeatureGate 'cluster': %w " , err )
4677 }
4778
4879 // Iterate through the feature gates status (typed structs)
@@ -68,3 +99,106 @@ func IsFeatureEnabled(ctx context.Context, featureName string) (bool, error) {
6899 framework .Logf ("Feature %s not found in FeatureGate status" , featureName )
69100 return false , nil
70101}
102+
103+ // GetCloudConfig retrieves the CCM cloud-config ConfigMap.
104+ // When cs is nil, a clientset is created from the current kubeconfig.
105+ // This function must not call Ginkgo control-flow helpers (Skip, Fail, etc.)
106+ // because it is also called from main.go outside a spec context.
107+ func GetCloudConfig (ctx context.Context , cs clientset.Interface ) (* v1.ConfigMap , error ) {
108+ var err error
109+ if cs == nil {
110+ cs , err = GetKubeClient (ctx )
111+ if err != nil {
112+ return nil , fmt .Errorf ("failed to get kubernetes client: %w" , err )
113+ }
114+ }
115+ cm , err := cs .CoreV1 ().ConfigMaps (cloudConfigNamespace ).Get (ctx , cloudConfigName , metav1.GetOptions {})
116+ if err != nil {
117+ return nil , fmt .Errorf ("failed to get cloud-config ConfigMap: %w" , err )
118+ }
119+ return cm , nil
120+ }
121+
122+ // IsConfigPresentCloudConfig checks if a specific configuration key is present in the
123+ // cloud-config data stored in the given ConfigMap. It searches all data entries for an
124+ // INI-style key=value match. Values are split by comma to support multi-value configs
125+ // e.g.: "ipFamilies = IPv4,IPv6" returns ["IPv4", "IPv6"], and
126+ // "NLBSecurityGroupMode" = "Managed" returns ["Managed"].
127+ func IsConfigPresentCloudConfig (cm * v1.ConfigMap , configKey string ) (bool , []string , error ) {
128+ if cm == nil {
129+ return false , nil , fmt .Errorf ("ConfigMap is nil" )
130+ }
131+ if configKey == "" {
132+ return false , nil , fmt .Errorf ("configKey is empty" )
133+ }
134+
135+ pattern , err := regexp .Compile (`(?m)^\s*` + regexp .QuoteMeta (configKey ) + `\s*=\s*(.*)$` )
136+ if err != nil {
137+ return false , nil , fmt .Errorf ("failed to compile regex for key %q: %w" , configKey , err )
138+ }
139+
140+ for dataKey , content := range cm .Data {
141+ allMatches := pattern .FindAllStringSubmatch (content , - 1 )
142+ if allMatches == nil {
143+ continue
144+ }
145+
146+ var values []string
147+ for _ , matches := range allMatches {
148+ rawValue := strings .TrimSpace (matches [1 ])
149+ if rawValue == "" {
150+ continue
151+ }
152+ for _ , p := range strings .Split (rawValue , "," ) {
153+ if v := strings .TrimSpace (p ); v != "" {
154+ values = append (values , v )
155+ }
156+ }
157+ }
158+
159+ framework .Logf ("Found key %q in ConfigMap data key %q with values: %v" , configKey , dataKey , values )
160+ return true , values , nil
161+ }
162+
163+ framework .Logf ("Key %q not found in ConfigMap %s/%s" , configKey , cm .Namespace , cm .Name )
164+ return false , nil , nil
165+ }
166+
167+ // IsNLBSecurityGroupModeManaged returns true when the cloud-config has
168+ // NLBSecurityGroupMode set to "Managed".
169+ func IsNLBSecurityGroupModeManaged (cm * v1.ConfigMap ) (bool , error ) {
170+ found , values , err := IsConfigPresentCloudConfig (cm , "NLBSecurityGroupMode" )
171+ if err != nil {
172+ return false , err
173+ }
174+ if ! found {
175+ return false , nil
176+ }
177+ return len (values ) == 1 && values [0 ] == "Managed" , nil
178+ }
179+
180+ // IsDualStack checks the NodeIPFamilies key in the cloud-config ConfigMap.
181+ // It returns (isDualStack, primaryIPv6, error) where isDualStack is true when
182+ // both IPv4 and IPv6 are present, and primaryIPv6 is true when the first
183+ // entry is IPv6 (e.g. NodeIPFamilies=ipv6 then NodeIPFamilies=ipv4).
184+ // When NodeIPFamilies is absent, both booleans are false with no error.
185+ func IsDualStack (cm * v1.ConfigMap ) (bool , bool , error ) {
186+ found , values , err := IsConfigPresentCloudConfig (cm , "NodeIPFamilies" )
187+ if err != nil {
188+ return false , false , fmt .Errorf ("failed to lookup up configuration NodeIPFamilies in cloud-config: %w" , err )
189+ }
190+ if ! found {
191+ return false , false , nil
192+ }
193+ var hasIPv4 , hasIPv6 bool
194+ for _ , ipFamily := range values {
195+ switch strings .ToLower (ipFamily ) {
196+ case "ipv6" :
197+ hasIPv6 = true
198+ case "ipv4" :
199+ hasIPv4 = true
200+ }
201+ }
202+ primaryIPv6 := len (values ) > 0 && strings .ToLower (values [0 ]) == "ipv6"
203+ return hasIPv4 && hasIPv6 , primaryIPv6 , nil
204+ }
0 commit comments