Skip to content

Commit

Permalink
Merge pull request #1754 from haircommander/min-kubelet-version
Browse files Browse the repository at this point in the history
OCPNODE-2940: add support for minimumKubeletVersion
  • Loading branch information
openshift-merge-bot[bot] authored Feb 28, 2025
2 parents 56e7346 + 0e78d80 commit 34af639
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 7 deletions.
5 changes: 0 additions & 5 deletions bindata/assets/config/defaultconfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ apiServerArguments:
- "true"
anonymous-auth:
- "true"
authorization-mode:
- Scope
- SystemMasters
- RBAC
- Node
audit-log-format:
- json
audit-log-maxbackup:
Expand Down
5 changes: 5 additions & 0 deletions pkg/cmd/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/openshift/cluster-kube-apiserver-operator/bindata"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/apienablement"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/auth"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation/node"
libgoaudit "github.com/openshift/library-go/pkg/operator/apiserver/audit"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
genericrender "github.com/openshift/library-go/pkg/operator/render"
Expand Down Expand Up @@ -357,6 +358,10 @@ func bootstrapDefaultConfig(featureGates featuregates.FeatureGate) ([]byte, erro
}
}

if err := node.AddAuthorizationModes(defaultConfig, featureGates.Enabled(features.FeatureGateMinimumKubeletVersion)); err != nil {
return nil, err
}

defaultConfigRaw, err := json.Marshal(defaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to marshal default config - %s", err)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func TestRenderCommand(t *testing.T) {
if !ok {
return fmt.Errorf("missing \"feature-gates\" entry in APIServerArguments")
}
expectedGates := []string{"Bar=false", "Foo=true", "OpenShiftPodSecurityAdmission=true"}
expectedGates := []string{"Bar=false", "Foo=true", "OpenShiftPodSecurityAdmission=true", "MinimumKubeletVersion=true"}
if len(actualGates) != len(expectedGates) {
return fmt.Errorf("expected to get exactly %d feature gates but found %d: expected=%v got=%v", len(expectedGates), len(actualGates), expectedGates, actualGates)
}
Expand Down Expand Up @@ -675,7 +675,7 @@ spec:
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ status:
enabled:
- name: Foo
- name: OpenShiftPodSecurityAdmission
- name: MinimumKubeletVersion
disabled:
- name: Bar
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ func NewConfigObserver(operatorClient v1helpers.StaticPodOperatorClient, kubeInf
),
},
),
node.NewMinimumKubeletVersionObserver(featureGateAccessor),
node.NewAuthorizationModeObserver(featureGateAccessor),
proxy.NewProxyObserveFunc([]string{"targetconfigcontroller", "proxy"}),
images.ObserveInternalRegistryHostname,
images.ObserveExternalRegistryHostnames,
Expand Down
81 changes: 81 additions & 0 deletions pkg/operator/configobservation/node/observe_authorization_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package node

import (
"github.com/openshift/api/features"
"github.com/openshift/library-go/pkg/operator/configobserver"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// There are scopes authorizer tests that fail if this order is changed.
// So this should not be sorted
var defaultAuthenticationModes = []string{
"Scope",
"SystemMasters",
"RBAC",
"Node",
}

var authenticationModesWithMinimumKubeletVersion = []string{
"Scope",
"SystemMasters",
"RBAC",
ModeMinimumKubeletVersion, // before "Node" to have a chance to deny a node
"Node",
}
var (
authModeFlag = "authorization-mode"
apiServerArgs = "apiServerArguments"
authModePath = []string{apiServerArgs, authModeFlag}
)

type authorizationModeObserver struct {
featureGateAccessor featuregates.FeatureGateAccess
authModes []string
}

func NewAuthorizationModeObserver(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
return (&authorizationModeObserver{
featureGateAccessor: featureGateAccessor,
}).ObserveAuthorizationMode
}

// ObserveAuthorizationMode watches the featuregate configuration and generates the apiServerArguments.authorization-mode
// It currently hardcodes the default set and adds MinimumKubeletVersion if the feature is set to on.
func (o *authorizationModeObserver) ObserveAuthorizationMode(genericListers configobserver.Listers, _ events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, errs []error) {
defer func() {
// Prune the observed config so that it only contains minimumKubeletVersion field.
ret = configobserver.Pruned(ret, authModePath)
}()

if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
return existingConfig, nil
}

featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
if err != nil {
return existingConfig, append(errs, err)
}

ret = map[string]interface{}{}
if err := AddAuthorizationModes(ret, featureGates.Enabled(features.FeatureGateMinimumKubeletVersion)); err != nil {
return existingConfig, append(errs, err)
}
return ret, nil
}

// AddAuthorizationModes modifies the passed in config
// to add the "authorization-mode": "MinimumKubeletVersion" if the feature is on. If it's off, it
// removes it instead.
// This function assumes MinimumKubeletVersion auth mode isn't present by default,
// and should likely be removed when it is.
func AddAuthorizationModes(observedConfig map[string]interface{}, isMinimumKubeletVersionEnabled bool) error {
modes := defaultAuthenticationModes
if isMinimumKubeletVersionEnabled {
modes = authenticationModesWithMinimumKubeletVersion
}

unstructured.RemoveNestedField(observedConfig, authModePath...)
return unstructured.SetNestedStringSlice(observedConfig, modes, authModePath...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package node

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestAddAuthorizationModes(t *testing.T) {
for _, on := range []bool{false, true} {
expectedSet := []any{"Scope", "SystemMasters", "RBAC", "Node"}
if on {
expectedSet = []any{"Scope", "SystemMasters", "RBAC", ModeMinimumKubeletVersion, "Node"}
}
for _, tc := range []struct {
name string
existingConfig map[string]interface{}
expectedConfig map[string]interface{}
}{
{
name: "should not fail if apiServerArguments not present",
existingConfig: map[string]interface{}{
"fakeconfig": "fake",
},
expectedConfig: map[string]interface{}{
"fakeconfig": "fake",
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
},
},
{
name: "should not fail if authorization-mode not present",
existingConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"fake": []any{"fake"}},
},
expectedConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"fake": []any{"fake"}, "authorization-mode": expectedSet},
},
},
{
name: "should clobber value if not expected",
existingConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"authorization-mode": []any{"fake"}},
},
expectedConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
},
},
{
name: "should not fail if MinimumKubeletVersion already present",
existingConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"authorization-mode": []any{"MinimumKubeletVersion"}},
},
expectedConfig: map[string]interface{}{
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
},
},
{
name: "should not fail if apiServerArguments not present",
existingConfig: map[string]interface{}{
"fakeconfig": "fake",
},
expectedConfig: map[string]interface{}{
"fakeconfig": "fake",
"apiServerArguments": map[string]any{"authorization-mode": expectedSet},
},
},
} {
name := tc.name + " when feature is "
if on {
name += "on"
} else {
name += "off"
}
t.Run(name, func(t *testing.T) {
if err := AddAuthorizationModes(tc.existingConfig, on); err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedConfig, tc.existingConfig); diff != "" {
t.Errorf("unexpected config:\n%s", diff)
}
})
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package node

import (
configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/api/features"
"github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation"
"github.com/openshift/library-go/pkg/operator/configobserver"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
"github.com/openshift/library-go/pkg/operator/events"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
)

var (
ModeMinimumKubeletVersion = "MinimumKubeletVersion"
minimumKubeletVersionConfigPath = "minimumKubeletVersion"
)

type minimumKubeletVersionObserver struct {
featureGateAccessor featuregates.FeatureGateAccess
}

func NewMinimumKubeletVersionObserver(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc {
return (&minimumKubeletVersionObserver{
featureGateAccessor: featureGateAccessor,
}).ObserveMinimumKubeletVersion
}

// ObserveKubeletMinimumVersion watches the node configuration and generates the minimumKubeletVersion
func (o *minimumKubeletVersionObserver) ObserveMinimumKubeletVersion(genericListers configobserver.Listers, _ events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, errs []error) {
defer func() {
// Prune the observed config so that it only contains minimumKubeletVersion field.
ret = configobserver.Pruned(ret, []string{minimumKubeletVersionConfigPath})
}()

if !o.featureGateAccessor.AreInitialFeatureGatesObserved() {
return existingConfig, nil
}

featureGates, err := o.featureGateAccessor.CurrentFeatureGates()
if err != nil {
return existingConfig, append(errs, err)
}

if !featureGates.Enabled(features.FeatureGateMinimumKubeletVersion) {
return existingConfig, nil
}

nodeLister := genericListers.(configobservation.Listers)
configNode, err := nodeLister.NodeLister().Get("cluster")
// we got an error so without the node object we are not able to determine minimumKubeletVersion
if err != nil {
// if config/v1/node/cluster object is not found, that can be treated as a non-error case, but raise a warning
if apierrors.IsNotFound(err) {
klog.Warningf("ObserveMinimumKubeletVersion: nodes.%s/cluster not found", configv1.GroupName)
} else {
errs = append(errs, err)
}
return existingConfig, errs
}

ret = map[string]interface{}{}
if configNode.Spec.MinimumKubeletVersion == "" {
// in case minimum kubelet version is not set on cluster
// return empty set of configs, this helps to unset the config
// values related to the minimumKubeletVersion.
// Also, ensures that this observer doesn't break cluster upgrades/downgrades
return ret, errs
}

if err := unstructured.SetNestedField(ret, configNode.Spec.MinimumKubeletVersion, minimumKubeletVersionConfigPath); err != nil {
return existingConfig, append(errs, err)
}

return ret, errs
}
Loading

0 comments on commit 34af639

Please sign in to comment.