Skip to content

Commit 34af639

Browse files
Merge pull request #1754 from haircommander/min-kubelet-version
OCPNODE-2940: add support for minimumKubeletVersion
2 parents 56e7346 + 0e78d80 commit 34af639

File tree

10 files changed

+363
-7
lines changed

10 files changed

+363
-7
lines changed

bindata/assets/config/defaultconfig.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ apiServerArguments:
3434
- "true"
3535
anonymous-auth:
3636
- "true"
37-
authorization-mode:
38-
- Scope
39-
- SystemMasters
40-
- RBAC
41-
- Node
4237
audit-log-format:
4338
- json
4439
audit-log-maxbackup:

pkg/cmd/render/render.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/openshift/cluster-kube-apiserver-operator/bindata"
2323
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/apienablement"
2424
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/auth"
25+
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/node"
2526
libgoaudit "github.com/openshift/library-go/pkg/operator/apiserver/audit"
2627
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
2728
genericrender "github.com/openshift/library-go/pkg/operator/render"
@@ -357,6 +358,10 @@ func bootstrapDefaultConfig(featureGates featuregates.FeatureGate) ([]byte, erro
357358
}
358359
}
359360

361+
if err := node.AddAuthorizationModes(defaultConfig, featureGates.Enabled(features.FeatureGateMinimumKubeletVersion)); err != nil {
362+
return nil, err
363+
}
364+
360365
defaultConfigRaw, err := json.Marshal(defaultConfig)
361366
if err != nil {
362367
return nil, fmt.Errorf("failed to marshal default config - %s", err)

pkg/cmd/render/render_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ func TestRenderCommand(t *testing.T) {
264264
if !ok {
265265
return fmt.Errorf("missing \"feature-gates\" entry in APIServerArguments")
266266
}
267-
expectedGates := []string{"Bar=false", "Foo=true", "OpenShiftPodSecurityAdmission=true"}
267+
expectedGates := []string{"Bar=false", "Foo=true", "OpenShiftPodSecurityAdmission=true", "MinimumKubeletVersion=true"}
268268
if len(actualGates) != len(expectedGates) {
269269
return fmt.Errorf("expected to get exactly %d feature gates but found %d: expected=%v got=%v", len(expectedGates), len(actualGates), expectedGates, actualGates)
270270
}
@@ -675,7 +675,7 @@ spec:
675675
}
676676

677677
func TestGetDefaultConfigWithAuditPolicy(t *testing.T) {
678-
raw, err := bootstrapDefaultConfig(featuregates.NewFeatureGate([]configv1.FeatureGateName{features.FeatureGateOpenShiftPodSecurityAdmission}, nil))
678+
raw, err := bootstrapDefaultConfig(featuregates.NewFeatureGate([]configv1.FeatureGateName{features.FeatureGateOpenShiftPodSecurityAdmission, features.FeatureGateMinimumKubeletVersion}, nil))
679679
require.NoError(t, err)
680680
require.True(t, len(raw) > 0)
681681

pkg/cmd/render/testdata/rendered/default-fg/featuregate.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ status:
88
enabled:
99
- name: Foo
1010
- name: OpenShiftPodSecurityAdmission
11+
- name: MinimumKubeletVersion
1112
disabled:
1213
- name: Bar

pkg/operator/configobservation/configobservercontroller/observe_config_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ func NewConfigObserver(operatorClient v1helpers.StaticPodOperatorClient, kubeInf
157157
),
158158
},
159159
),
160+
node.NewMinimumKubeletVersionObserver(featureGateAccessor),
161+
node.NewAuthorizationModeObserver(featureGateAccessor),
160162
proxy.NewProxyObserveFunc([]string{"targetconfigcontroller", "proxy"}),
161163
images.ObserveInternalRegistryHostname,
162164
images.ObserveExternalRegistryHostnames,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package node
2+
3+
import (
4+
"github.com/openshift/api/features"
5+
"github.com/openshift/library-go/pkg/operator/configobserver"
6+
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
7+
"github.com/openshift/library-go/pkg/operator/events"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
)
10+
11+
// There are scopes authorizer tests that fail if this order is changed.
12+
// So this should not be sorted
13+
var defaultAuthenticationModes = []string{
14+
"Scope",
15+
"SystemMasters",
16+
"RBAC",
17+
"Node",
18+
}
19+
20+
var authenticationModesWithMinimumKubeletVersion = []string{
21+
"Scope",
22+
"SystemMasters",
23+
"RBAC",
24+
ModeMinimumKubeletVersion, // before "Node" to have a chance to deny a node
25+
"Node",
26+
}
27+
var (
28+
authModeFlag = "authorization-mode"
29+
apiServerArgs = "apiServerArguments"
30+
authModePath = []string{apiServerArgs, authModeFlag}
31+
)
32+
33+
type authorizationModeObserver struct {
34+
featureGateAccessor featuregates.FeatureGateAccess
35+
authModes []string
36+
}
37+
38+
func NewAuthorizationModeObserver(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
39+
return (&authorizationModeObserver{
40+
featureGateAccessor: featureGateAccessor,
41+
}).ObserveAuthorizationMode
42+
}
43+
44+
// ObserveAuthorizationMode watches the featuregate configuration and generates the apiServerArguments.authorization-mode
45+
// It currently hardcodes the default set and adds MinimumKubeletVersion if the feature is set to on.
46+
func (o *authorizationModeObserver) ObserveAuthorizationMode(genericListers configobserver.Listers, _ events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, errs []error) {
47+
defer func() {
48+
// Prune the observed config so that it only contains minimumKubeletVersion field.
49+
ret = configobserver.Pruned(ret, authModePath)
50+
}()
51+
52+
if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
53+
return existingConfig, nil
54+
}
55+
56+
featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
57+
if err != nil {
58+
return existingConfig, append(errs, err)
59+
}
60+
61+
ret = map[string]interface{}{}
62+
if err := AddAuthorizationModes(ret, featureGates.Enabled(features.FeatureGateMinimumKubeletVersion)); err != nil {
63+
return existingConfig, append(errs, err)
64+
}
65+
return ret, nil
66+
}
67+
68+
// AddAuthorizationModes modifies the passed in config
69+
// to add the "authorization-mode": "MinimumKubeletVersion" if the feature is on. If it's off, it
70+
// removes it instead.
71+
// This function assumes MinimumKubeletVersion auth mode isn't present by default,
72+
// and should likely be removed when it is.
73+
func AddAuthorizationModes(observedConfig map[string]interface{}, isMinimumKubeletVersionEnabled bool) error {
74+
modes := defaultAuthenticationModes
75+
if isMinimumKubeletVersionEnabled {
76+
modes = authenticationModesWithMinimumKubeletVersion
77+
}
78+
79+
unstructured.RemoveNestedField(observedConfig, authModePath...)
80+
return unstructured.SetNestedStringSlice(observedConfig, modes, authModePath...)
81+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package node
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func TestAddAuthorizationModes(t *testing.T) {
10+
for _, on := range []bool{false, true} {
11+
expectedSet := []any{"Scope", "SystemMasters", "RBAC", "Node"}
12+
if on {
13+
expectedSet = []any{"Scope", "SystemMasters", "RBAC", ModeMinimumKubeletVersion, "Node"}
14+
}
15+
for _, tc := range []struct {
16+
name string
17+
existingConfig map[string]interface{}
18+
expectedConfig map[string]interface{}
19+
}{
20+
{
21+
name: "should not fail if apiServerArguments not present",
22+
existingConfig: map[string]interface{}{
23+
"fakeconfig": "fake",
24+
},
25+
expectedConfig: map[string]interface{}{
26+
"fakeconfig": "fake",
27+
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
28+
},
29+
},
30+
{
31+
name: "should not fail if authorization-mode not present",
32+
existingConfig: map[string]interface{}{
33+
"apiServerArguments": map[string]any{"fake": []any{"fake"}},
34+
},
35+
expectedConfig: map[string]interface{}{
36+
"apiServerArguments": map[string]any{"fake": []any{"fake"}, "authorization-mode": expectedSet},
37+
},
38+
},
39+
{
40+
name: "should clobber value if not expected",
41+
existingConfig: map[string]interface{}{
42+
"apiServerArguments": map[string]any{"authorization-mode": []any{"fake"}},
43+
},
44+
expectedConfig: map[string]interface{}{
45+
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
46+
},
47+
},
48+
{
49+
name: "should not fail if MinimumKubeletVersion already present",
50+
existingConfig: map[string]interface{}{
51+
"apiServerArguments": map[string]any{"authorization-mode": []any{"MinimumKubeletVersion"}},
52+
},
53+
expectedConfig: map[string]interface{}{
54+
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
55+
},
56+
},
57+
{
58+
name: "should not fail if apiServerArguments not present",
59+
existingConfig: map[string]interface{}{
60+
"fakeconfig": "fake",
61+
},
62+
expectedConfig: map[string]interface{}{
63+
"fakeconfig": "fake",
64+
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
65+
},
66+
},
67+
} {
68+
name := tc.name + " when feature is "
69+
if on {
70+
name += "on"
71+
} else {
72+
name += "off"
73+
}
74+
t.Run(name, func(t *testing.T) {
75+
if err := AddAuthorizationModes(tc.existingConfig, on); err != nil {
76+
t.Fatal(err)
77+
}
78+
79+
if diff := cmp.Diff(tc.expectedConfig, tc.existingConfig); diff != "" {
80+
t.Errorf("unexpected config:\n%s", diff)
81+
}
82+
})
83+
}
84+
}
85+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package node
2+
3+
import (
4+
configv1 "github.com/openshift/api/config/v1"
5+
"github.com/openshift/api/features"
6+
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
7+
"github.com/openshift/library-go/pkg/operator/configobserver"
8+
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
9+
"github.com/openshift/library-go/pkg/operator/events"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12+
"k8s.io/klog/v2"
13+
)
14+
15+
var (
16+
ModeMinimumKubeletVersion = "MinimumKubeletVersion"
17+
minimumKubeletVersionConfigPath = "minimumKubeletVersion"
18+
)
19+
20+
type minimumKubeletVersionObserver struct {
21+
featureGateAccessor featuregates.FeatureGateAccess
22+
}
23+
24+
func NewMinimumKubeletVersionObserver(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
25+
return (&minimumKubeletVersionObserver{
26+
featureGateAccessor: featureGateAccessor,
27+
}).ObserveMinimumKubeletVersion
28+
}
29+
30+
// ObserveKubeletMinimumVersion watches the node configuration and generates the minimumKubeletVersion
31+
func (o *minimumKubeletVersionObserver) ObserveMinimumKubeletVersion(genericListers configobserver.Listers, _ events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, errs []error) {
32+
defer func() {
33+
// Prune the observed config so that it only contains minimumKubeletVersion field.
34+
ret = configobserver.Pruned(ret, []string{minimumKubeletVersionConfigPath})
35+
}()
36+
37+
if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
38+
return existingConfig, nil
39+
}
40+
41+
featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
42+
if err != nil {
43+
return existingConfig, append(errs, err)
44+
}
45+
46+
if !featureGates.Enabled(features.FeatureGateMinimumKubeletVersion) {
47+
return existingConfig, nil
48+
}
49+
50+
nodeLister := genericListers.(configobservation.Listers)
51+
configNode, err := nodeLister.NodeLister().Get("cluster")
52+
// we got an error so without the node object we are not able to determine minimumKubeletVersion
53+
if err != nil {
54+
// if config/v1/node/cluster object is not found, that can be treated as a non-error case, but raise a warning
55+
if apierrors.IsNotFound(err) {
56+
klog.Warningf("ObserveMinimumKubeletVersion: nodes.%s/cluster not found", configv1.GroupName)
57+
} else {
58+
errs = append(errs, err)
59+
}
60+
return existingConfig, errs
61+
}
62+
63+
ret = map[string]interface{}{}
64+
if configNode.Spec.MinimumKubeletVersion == "" {
65+
// in case minimum kubelet version is not set on cluster
66+
// return empty set of configs, this helps to unset the config
67+
// values related to the minimumKubeletVersion.
68+
// Also, ensures that this observer doesn't break cluster upgrades/downgrades
69+
return ret, errs
70+
}
71+
72+
if err := unstructured.SetNestedField(ret, configNode.Spec.MinimumKubeletVersion, minimumKubeletVersionConfigPath); err != nil {
73+
return existingConfig, append(errs, err)
74+
}
75+
76+
return ret, errs
77+
}

0 commit comments

Comments
 (0)