Skip to content

Commit 902d1e2

Browse files
committed
Gate DRA reconciler on Openshift >= 4.21
The DRA reconciler was registered unconditionally, which would crash the entire operator on clusters older than 4.21 because the resource.k8s.io/v1 DeviceClass API does not exist there. Introduce an internal/version package with OCPVersion, DiscoverOCPVersion, and an AtLeast method to centralise version discovery and comparison. The manager now checks the cluster version at startup and only registers the DRA controller when the minimum version is met. The webhook package is updated to use the shared version package instead of its own local types. Also fixes a pre-existing bug where /run was missing from the allowed hostPath prefixes error message.
1 parent 080def7 commit 902d1e2

7 files changed

Lines changed: 195 additions & 112 deletions

File tree

cmd/manager/main.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/rh-ecosystem-edge/kernel-module-management/internal/networkpolicy"
4949
"github.com/rh-ecosystem-edge/kernel-module-management/internal/nmc"
5050
"github.com/rh-ecosystem-edge/kernel-module-management/internal/syncronizedmap"
51+
"github.com/rh-ecosystem-edge/kernel-module-management/internal/version"
5152
"k8s.io/apimachinery/pkg/api/meta"
5253
runtimescheme "k8s.io/apimachinery/pkg/runtime/schema"
5354

@@ -126,8 +127,16 @@ func main() {
126127
}
127128

128129
options := cg.GetManagerOptionsFromConfig(cfg, scheme)
130+
restCfg := ctrl.GetConfigOrDie()
129131

130-
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
132+
ocpVersion, err := version.DiscoverOCPVersion(restCfg)
133+
if err != nil {
134+
cmd.FatalError(setupLogger, err, "unable to discover Openshift version")
135+
}
136+
137+
setupLogger.Info("Detected Openshift version", "major", ocpVersion.Major, "minor", ocpVersion.Minor)
138+
139+
mgr, err := ctrl.NewManager(restCfg, options)
131140
if err != nil {
132141
cmd.FatalError(setupLogger, err, "unable to create manager")
133142
}
@@ -195,8 +204,16 @@ func main() {
195204
cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.PodNodeLabelReconcilerName)
196205
}
197206

198-
if err = controllers.NewDRAReconciler(client, nodeAPI, networkPolicyAPI, scheme, operatorNamespace).SetupWithManager(mgr); err != nil {
199-
cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.DRAReconcilerName)
207+
if ocpVersion.AtLeast(constants.MinOCPMajorForDRA, constants.MinOCPMinorForDRA) {
208+
if err = controllers.NewDRAReconciler(client, nodeAPI, networkPolicyAPI, scheme, operatorNamespace).SetupWithManager(mgr); err != nil {
209+
cmd.FatalError(setupLogger, err, "unable to create controller", "name", controllers.DRAReconcilerName)
210+
}
211+
} else {
212+
setupLogger.Info(
213+
fmt.Sprintf("Skipping DRA controller; requires Openshift >= %d.%d", constants.MinOCPMajorForDRA, constants.MinOCPMinorForDRA),
214+
"detectedMajor", ocpVersion.Major,
215+
"detectedMinor", ocpVersion.Minor,
216+
)
200217
}
201218

202219
if err = controllers.NewNodeLabelModuleVersionReconciler(client).SetupWithManager(mgr); err != nil {

cmd/webhook-server/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/rh-ecosystem-edge/kernel-module-management/internal/cmd"
1010
"github.com/rh-ecosystem-edge/kernel-module-management/internal/config"
1111
"github.com/rh-ecosystem-edge/kernel-module-management/internal/constants"
12+
"github.com/rh-ecosystem-edge/kernel-module-management/internal/version"
1213
"github.com/rh-ecosystem-edge/kernel-module-management/internal/webhook"
1314
"github.com/rh-ecosystem-edge/kernel-module-management/internal/webhook/hub"
1415
"k8s.io/apimachinery/pkg/runtime"
@@ -84,7 +85,7 @@ func main() {
8485
if enableModule {
8586
logger.Info("Enabling Module webhook")
8687

87-
ocpVersion, err := webhook.DiscoverOCPVersion(restCfg)
88+
ocpVersion, err := version.DiscoverOCPVersion(restCfg)
8889
if err != nil {
8990
cmd.FatalError(setupLogger, err, "unable to discover OpenShift version")
9091
}

internal/version/version.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package version
18+
19+
import (
20+
"fmt"
21+
"regexp"
22+
"strconv"
23+
24+
"k8s.io/client-go/discovery"
25+
"k8s.io/client-go/rest"
26+
)
27+
28+
var ocpVersionRe = regexp.MustCompile(`^v?(\d+)\.(\d+)`)
29+
30+
// OCPVersion holds the parsed major and minor version of a OpenShift cluster.
31+
type OCPVersion struct {
32+
Major int
33+
Minor int
34+
}
35+
36+
// AtLeast returns true if kv is greater than or equal to the given major.minor version.
37+
func (ov OCPVersion) AtLeast(major, minor int) bool {
38+
return ov.Major > major || (ov.Major == major && ov.Minor >= minor)
39+
}
40+
41+
// DiscoverOCPVersion queries the OpenShift API server and returns its version.
42+
func DiscoverOCPVersion(cfg *rest.Config) (OCPVersion, error) {
43+
discClient, err := discovery.NewDiscoveryClientForConfig(cfg)
44+
if err != nil {
45+
return OCPVersion{}, fmt.Errorf("failed to create discovery client: %v", err)
46+
}
47+
48+
serverVersion, err := discClient.ServerVersion()
49+
if err != nil {
50+
return OCPVersion{}, fmt.Errorf("failed to query OpenShift server version: %v", err)
51+
}
52+
53+
return ParseOCPVersion(serverVersion.GitVersion)
54+
}
55+
56+
// ParseOCPVersion extracts the major and minor version from a OpenShift
57+
// GitVersion string such as "v4.21.0" or "v4.21.0+ocp-4.21.0".
58+
func ParseOCPVersion(gitVersion string) (OCPVersion, error) {
59+
m := ocpVersionRe.FindStringSubmatch(gitVersion)
60+
if m == nil {
61+
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift version from %q", gitVersion)
62+
}
63+
64+
major, err := strconv.Atoi(m[1])
65+
if err != nil {
66+
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift major version from %q: %w", gitVersion, err)
67+
}
68+
minor, err := strconv.Atoi(m[2])
69+
if err != nil {
70+
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift minor version from %q: %w", gitVersion, err)
71+
}
72+
return OCPVersion{Major: major, Minor: minor}, nil
73+
}

internal/version/version_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package version
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
)
25+
26+
func TestSuite(t *testing.T) {
27+
RegisterFailHandler(Fail)
28+
RunSpecs(t, "Version Suite")
29+
}
30+
31+
var _ = Describe("AtLeast", func() {
32+
DescribeTable(
33+
"should compare versions correctly",
34+
func(major, minor, minMajor, minMinor int, expected bool) {
35+
ov := OCPVersion{Major: major, Minor: minor}
36+
Expect(ov.AtLeast(minMajor, minMinor)).To(Equal(expected))
37+
},
38+
Entry("exact match", 4, 21, 4, 21, true),
39+
Entry("higher minor", 4, 22, 4, 21, true),
40+
Entry("lower minor", 4, 20, 4, 21, false),
41+
Entry("higher major, lower minor", 5, 0, 4, 21, true),
42+
Entry("lower major", 3, 11, 4, 21, false),
43+
)
44+
})
45+
46+
var _ = Describe("ParseKubeVersion", func() {
47+
DescribeTable(
48+
"should parse valid version strings",
49+
func(gitVersion string, expectedMajor, expectedMinor int) {
50+
ov, err := ParseOCPVersion(gitVersion)
51+
Expect(err).NotTo(HaveOccurred())
52+
Expect(ov.Major).To(Equal(expectedMajor))
53+
Expect(ov.Minor).To(Equal(expectedMinor))
54+
},
55+
Entry("standard version", "v4.21.0", 4, 21),
56+
Entry("with build metadata", "v4.21.0+ocp-4.21.0", 4, 21),
57+
Entry("pre-release version", "v4.20.3-rc.1", 4, 20),
58+
Entry("higher minor", "v4.22.1", 4, 22),
59+
Entry("without v prefix", "4.21.0", 4, 21),
60+
Entry("hypothetical OCP 5.0", "5.0.0", 5, 0),
61+
Entry("hypothetical OCP 2.5", "2.5.3", 2, 5),
62+
)
63+
64+
DescribeTable(
65+
"should return error for invalid strings",
66+
func(gitVersion string) {
67+
_, err := ParseOCPVersion(gitVersion)
68+
Expect(err).To(HaveOccurred())
69+
},
70+
Entry("empty string", ""),
71+
Entry("garbage", "invalid"),
72+
Entry("no minor", "v1"),
73+
)
74+
})

internal/webhook/hub/managedclustermodule_webhook.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/go-logr/logr"
2424
"github.com/rh-ecosystem-edge/kernel-module-management/api-hub/v1beta1"
2525
kmmv1beta1 "github.com/rh-ecosystem-edge/kernel-module-management/api/v1beta1"
26+
"github.com/rh-ecosystem-edge/kernel-module-management/internal/version"
2627
"github.com/rh-ecosystem-edge/kernel-module-management/internal/webhook"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
ctrl "sigs.k8s.io/controller-runtime"
@@ -34,7 +35,7 @@ type ManagedClusterModuleValidator struct {
3435
m admission.CustomValidator
3536
}
3637

37-
func NewManagedClusterModuleValidator(logger logr.Logger, ocpVersion *webhook.OCPVersion) *ManagedClusterModuleValidator {
38+
func NewManagedClusterModuleValidator(logger logr.Logger, ocpVersion *version.OCPVersion) *ManagedClusterModuleValidator {
3839
return &ManagedClusterModuleValidator{
3940
logger: logger,
4041
m: webhook.NewModuleValidator(logger, ocpVersion),

internal/webhook/module.go

Lines changed: 13 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,17 @@ import (
2222
"fmt"
2323
"path/filepath"
2424
"regexp"
25-
"strconv"
2625
"strings"
2726

2827
corev1 "k8s.io/api/core/v1"
2928
"k8s.io/apimachinery/pkg/util/validation"
3029

3130
"github.com/go-logr/logr"
32-
configv1 "github.com/openshift/api/config/v1"
3331
kmmv1beta1 "github.com/rh-ecosystem-edge/kernel-module-management/api/v1beta1"
3432
"github.com/rh-ecosystem-edge/kernel-module-management/internal/constants"
33+
"github.com/rh-ecosystem-edge/kernel-module-management/internal/version"
3534
"k8s.io/apimachinery/pkg/runtime"
36-
"k8s.io/apimachinery/pkg/runtime/schema"
37-
"k8s.io/apimachinery/pkg/runtime/serializer"
3835
"k8s.io/apimachinery/pkg/util/sets"
39-
"k8s.io/client-go/rest"
4036
ctrl "sigs.k8s.io/controller-runtime"
4137
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
4238
)
@@ -45,66 +41,15 @@ import (
4541
// 63 (max label key length after slash) - len("version-schedule-plugin.") = 38
4642
const maxCombinedLength = 38
4743

48-
var ocpVersionRe = regexp.MustCompile(`^v?(\d+)\.(\d+)`)
49-
50-
// OCPVersion holds the parsed major and minor version of an OpenShift cluster.
51-
type OCPVersion struct {
52-
Major int
53-
Minor int
54-
}
55-
5644
type ModuleValidator struct {
5745
logger logr.Logger
58-
ocpVersion *OCPVersion
46+
ocpVersion *version.OCPVersion
5947
}
6048

61-
func NewModuleValidator(logger logr.Logger, ocpVersion *OCPVersion) *ModuleValidator {
49+
func NewModuleValidator(logger logr.Logger, ocpVersion *version.OCPVersion) *ModuleValidator {
6250
return &ModuleValidator{logger: logger, ocpVersion: ocpVersion}
6351
}
6452

65-
// DiscoverOCPVersion queries the OpenShift ClusterVersion resource and returns the cluster version.
66-
func DiscoverOCPVersion(cfg *rest.Config) (OCPVersion, error) {
67-
ocpScheme := runtime.NewScheme()
68-
if err := configv1.Install(ocpScheme); err != nil {
69-
return OCPVersion{}, fmt.Errorf("failed to register OpenShift config scheme: %v", err)
70-
}
71-
72-
cfgCopy := *cfg
73-
cfgCopy.GroupVersion = &schema.GroupVersion{Group: "config.openshift.io", Version: "v1"}
74-
cfgCopy.APIPath = "/apis"
75-
cfgCopy.NegotiatedSerializer = serializer.NewCodecFactory(ocpScheme)
76-
77-
client, err := rest.RESTClientFor(&cfgCopy)
78-
if err != nil {
79-
return OCPVersion{}, fmt.Errorf("failed to create REST client for OpenShift config API: %v", err)
80-
}
81-
82-
cv := &configv1.ClusterVersion{}
83-
if err = client.Get().Resource("clusterversions").Name("version").Do(context.Background()).Into(cv); err != nil {
84-
return OCPVersion{}, fmt.Errorf("failed to get ClusterVersion: %v", err)
85-
}
86-
87-
return parseOCPVersion(cv.Status.Desired.Version)
88-
}
89-
90-
// parseOCPVersion extracts the major and minor version from an OpenShift
91-
// version string such as "4.21.0" or "4.21.0-rc.1".
92-
func parseOCPVersion(version string) (OCPVersion, error) {
93-
m := ocpVersionRe.FindStringSubmatch(version)
94-
if m == nil {
95-
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift version from %q", version)
96-
}
97-
major, err := strconv.Atoi(m[1])
98-
if err != nil {
99-
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift major version from %q: %v", version, err)
100-
}
101-
minor, err := strconv.Atoi(m[2])
102-
if err != nil {
103-
return OCPVersion{}, fmt.Errorf("cannot parse OpenShift minor version from %q: %v", version, err)
104-
}
105-
return OCPVersion{Major: major, Minor: minor}, nil
106-
}
107-
10853
func (m *ModuleValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
10954
// controller-runtime will set the path to `validate-<group>-<version>-<resource> so we
11055
// need to make sure it is set correctly in the +kubebuilder annotation below.
@@ -157,7 +102,7 @@ func (m *ModuleValidator) ValidateDelete(ctx context.Context, obj runtime.Object
157102
return nil, NotImplemented
158103
}
159104

160-
func validateModule(mod *kmmv1beta1.Module, ocpVersion *OCPVersion) (admission.Warnings, error) {
105+
func validateModule(mod *kmmv1beta1.Module, ocpVersion *version.OCPVersion) (admission.Warnings, error) {
161106
nameLength := len(mod.Name + mod.Namespace)
162107

163108
if nameLength > maxCombinedLength {
@@ -204,18 +149,18 @@ func validateModule(mod *kmmv1beta1.Module, ocpVersion *OCPVersion) (admission.W
204149
return nil, validateFilesToSign(mod.Spec.ModuleLoader.Container)
205150
}
206151

207-
func validateDRA(mod *kmmv1beta1.Module, ocpVersion *OCPVersion) error {
152+
func validateDRA(mod *kmmv1beta1.Module, ocpVersion *version.OCPVersion) error {
208153
if mod.Spec.DRA == nil {
209154
return nil
210155
}
211156

212-
if ocpVersion != nil {
213-
if ocpVersion.Major < constants.MinOCPMajorForDRA || (ocpVersion.Major == constants.MinOCPMajorForDRA && ocpVersion.Minor < constants.MinOCPMinorForDRA) {
214-
return fmt.Errorf(
215-
"spec.dra requires OpenShift %d.%d or later; current cluster version is %d.%d",
216-
constants.MinOCPMajorForDRA, constants.MinOCPMinorForDRA, ocpVersion.Major, ocpVersion.Minor,
217-
)
218-
}
157+
// Skip version gating when ocpVersion is nil (e.g. hub webhook validating
158+
// a ManagedClusterModule — the spoke's own webhook will enforce its version).
159+
if ocpVersion != nil && !ocpVersion.AtLeast(constants.MinOCPMajorForDRA, constants.MinOCPMinorForDRA) {
160+
return fmt.Errorf(
161+
"spec.dra requires OpenShift %d.%d or later; current cluster version is %d.%d",
162+
constants.MinOCPMajorForDRA, constants.MinOCPMinorForDRA, ocpVersion.Major, ocpVersion.Minor,
163+
)
219164
}
220165
driverName := mod.Spec.DRA.DriverName
221166
if driverName == "" {
@@ -261,7 +206,7 @@ func validateHostPathVolumes(fieldPath string, volumes []corev1.Volume) error {
261206
for i, vol := range volumes {
262207
if vol.HostPath != nil && !isAllowedHostPath(vol.HostPath.Path) {
263208
return fmt.Errorf(
264-
"%s.volumes[%d]: hostPath %q is not allowed; only /dev, /sys, /var and /opt paths are permitted",
209+
"%s.volumes[%d]: hostPath %q is not allowed; only /dev, /sys, /var, /opt and /run paths are permitted",
265210
fieldPath, i, vol.HostPath.Path,
266211
)
267212
}

0 commit comments

Comments
 (0)