diff --git a/go.mod b/go.mod index 275354c013..30ba876111 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/openshift/api v0.0.0-20230810152202-3e3f07aadec4 github.com/openshift/build-machinery-go v0.0.0-20230228230858-4cd708338479 github.com/openshift/client-go v0.0.0-20230503144108-75015d2347cb - github.com/openshift/library-go v0.0.0-20231103161458-0ec67489d123 + github.com/openshift/library-go v0.0.0-20231213084759-840298df1eee github.com/pkg/profile v1.5.0 // indirect github.com/prometheus/client_golang v1.14.0 github.com/spf13/cobra v1.6.1 @@ -30,6 +30,7 @@ require ( k8s.io/client-go v0.27.4 k8s.io/component-base v0.27.4 k8s.io/klog/v2 v2.100.1 + k8s.io/pod-security-admission v0.27.4 k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 ) diff --git a/go.sum b/go.sum index 8227b0809b..546e2823f3 100644 --- a/go.sum +++ b/go.sum @@ -293,8 +293,8 @@ github.com/openshift/build-machinery-go v0.0.0-20230228230858-4cd708338479 h1:IU github.com/openshift/build-machinery-go v0.0.0-20230228230858-4cd708338479/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= github.com/openshift/client-go v0.0.0-20230503144108-75015d2347cb h1:Nij5OnaECrkmcRQMAE9LMbQXPo95aqFnf+12B7SyFVI= github.com/openshift/client-go v0.0.0-20230503144108-75015d2347cb/go.mod h1:Rhb3moCqeiTuGHAbXBOlwPubUMlOZEkrEWTRjIF3jzs= -github.com/openshift/library-go v0.0.0-20231103161458-0ec67489d123 h1:JfXG50f8yVud5xakwTHoqD00+3HYdLmZuEqn5Sq8ZRQ= -github.com/openshift/library-go v0.0.0-20231103161458-0ec67489d123/go.mod h1:ZFwNwC3opc/7aOvzUbU95zp33Lbxet48h80ryH3p6DY= +github.com/openshift/library-go v0.0.0-20231213084759-840298df1eee h1:pHWQZMxHsE841/QjJEfM4vNl+PrjcrdWk0Qn4MjNhuk= +github.com/openshift/library-go v0.0.0-20231213084759-840298df1eee/go.mod h1:ZFwNwC3opc/7aOvzUbU95zp33Lbxet48h80ryH3p6DY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -771,6 +771,8 @@ k8s.io/kube-aggregator v0.27.4 h1:WdK9iiBr32G8bWfpUEFVQl70RZO2dU19ZAktUXL5JFc= k8s.io/kube-aggregator v0.27.4/go.mod h1:+eG83gkAyh0uilQEAOgheeQW4hr+PkyV+5O1nLGsjlM= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/pod-security-admission v0.27.4 h1:AA32ID+ECNJoUU8yuzLt4WzKPDZg7zMmP2cZ9rVsFyE= +k8s.io/pod-security-admission v0.27.4/go.mod h1:GOcnrXk8TT5cPhtCxdlkOAvBnX3QmZiMHqPw9PbZhPs= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions.go b/pkg/operator/podsecurityreadinesscontroller/conditions.go new file mode 100644 index 0000000000..189282fcd8 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/conditions.go @@ -0,0 +1,80 @@ +package podsecurityreadinesscontroller + +import ( + "fmt" + "sort" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + + operatorv1 "github.com/openshift/api/operator/v1" + "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +const ( + PodSecurityCustomerType = "PodSecurityCustomerEvaluationConditionsDetected" + PodSecurityOpenshiftType = "PodSecurityOpenshiftEvaluationConditionsDetected" + PodSecurityRunLevelZeroType = "PodSecurityRunLevelZeroEvaluationConditionsDetected" +) + +var ( + // run-level zero namespaces, shouldn't avoid openshift namespaces + runLevelZeroNamespaces = sets.New[string]( + "default", + "kube-system", + "kube-public", + ) +) + +type podSecurityOperatorConditions struct { + violatingOpenShiftNamespaces []string + violatingRunLevelZeroNamespaces []string + violatingCustomerNamespaces []string +} + +func (c *podSecurityOperatorConditions) addViolation(name string) { + if runLevelZeroNamespaces.Has(name) { + c.violatingRunLevelZeroNamespaces = append(c.violatingRunLevelZeroNamespaces, name) + return + } + + isOpenShift := strings.HasPrefix(name, "openshift") + if isOpenShift { + c.violatingOpenShiftNamespaces = append(c.violatingOpenShiftNamespaces, name) + return + } + + c.violatingCustomerNamespaces = append(c.violatingCustomerNamespaces, name) +} + +func makeCondition(conditionType string, namespaces []string) operatorv1.OperatorCondition { + if len(namespaces) > 0 { + sort.Strings(namespaces) + return operatorv1.OperatorCondition{ + Type: conditionType, + Status: operatorv1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "PSViolationsDetected", + Message: fmt.Sprintf( + "Violations detected in namespaces: %v", + namespaces, + ), + } + } + + return operatorv1.OperatorCondition{ + Type: conditionType, + Status: operatorv1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: "ExpectedReason", + } +} + +func (c *podSecurityOperatorConditions) toConditionFuncs() []v1helpers.UpdateStatusFunc { + return []v1helpers.UpdateStatusFunc{ + v1helpers.UpdateConditionFn(makeCondition(PodSecurityCustomerType, c.violatingCustomerNamespaces)), + v1helpers.UpdateConditionFn(makeCondition(PodSecurityOpenshiftType, c.violatingOpenShiftNamespaces)), + v1helpers.UpdateConditionFn(makeCondition(PodSecurityRunLevelZeroType, c.violatingRunLevelZeroNamespaces)), + } +} diff --git a/pkg/operator/podsecurityreadinesscontroller/conditions_test.go b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go new file mode 100644 index 0000000000..55dd2dbd94 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/conditions_test.go @@ -0,0 +1,69 @@ +package podsecurityreadinesscontroller + +import ( + "testing" + + operatorv1 "github.com/openshift/api/operator/v1" +) + +func TestCondition(t *testing.T) { + t.Run("with namespaces", func(t *testing.T) { + namespaces := []string{"namespace1", "namespace2"} + expectedCondition := operatorv1.OperatorCondition{ + Type: PodSecurityCustomerType, + Status: operatorv1.ConditionTrue, + Reason: "PSViolationsDetected", + Message: "Violations detected in namespaces: [namespace1 namespace2]", + } + + condition := makeCondition(PodSecurityCustomerType, namespaces) + + if condition.Type != expectedCondition.Type { + t.Errorf("expected condition type %s, got %s", expectedCondition.Type, condition.Type) + } + + if condition.Status != expectedCondition.Status { + t.Errorf("expected condition status %s, got %s", expectedCondition.Status, condition.Status) + } + + if condition.Reason != expectedCondition.Reason { + t.Errorf("expected condition reason %s, got %s", expectedCondition.Reason, condition.Reason) + } + + if condition.Message != expectedCondition.Message { + t.Errorf("expected condition message %s, got %s", expectedCondition.Message, condition.Message) + } + }) + + t.Run("without namespaces", func(t *testing.T) { + namespaces := []string{} + expectedCondition := operatorv1.OperatorCondition{ + Type: PodSecurityCustomerType, + Status: operatorv1.ConditionFalse, + Reason: "ExpectedReason", + } + + condition := makeCondition(PodSecurityCustomerType, namespaces) + + if condition.Type != expectedCondition.Type { + t.Errorf("expected condition type %s, got %s", expectedCondition.Type, condition.Type) + } + + if condition.Status != expectedCondition.Status { + t.Errorf("expected condition status %s, got %s", expectedCondition.Status, condition.Status) + } + + if condition.Reason != expectedCondition.Reason { + t.Errorf("expected condition reason %s, got %s", expectedCondition.Reason, condition.Reason) + } + + if condition.Message != expectedCondition.Message { + t.Errorf("expected condition message %s, got %s", expectedCondition.Message, condition.Message) + } + }) + + t.Run("without anything", func(t *testing.T) { + cond := podSecurityOperatorConditions{} + cond.addViolation("hello world") + }) +} diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go new file mode 100644 index 0000000000..ee4be62008 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller.go @@ -0,0 +1,171 @@ +package podsecurityreadinesscontroller + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + applyconfiguration "k8s.io/client-go/applyconfigurations/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + psapi "k8s.io/pod-security-admission/api" + + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/v1helpers" +) + +const ( + checkInterval = 240 * time.Minute // Adjust the interval as needed. +) + +var podSecurityAlertLabels = []string{ + psapi.AuditLevelLabel, + psapi.WarnLevelLabel, +} + +// PodSecurityReadinessController checks if namespaces are ready for Pod Security Admission enforcement. +type PodSecurityReadinessController struct { + kubeClient kubernetes.Interface + operatorClient v1helpers.OperatorClient + + warningsHandler *warningsHandler + namespaceSelector string +} + +func NewPodSecurityReadinessController( + kubeConfig *rest.Config, + operatorClient v1helpers.OperatorClient, + recorder events.Recorder, +) (factory.Controller, error) { + warningsHandler := &warningsHandler{} + + kubeClientCopy := rest.CopyConfig(kubeConfig) + kubeClientCopy.WarningHandler = warningsHandler + // We don't want to overwhelm the apiserver with requests. On a cluster with + // 10k namespaces, we would send 10k + 1 requests to the apiserver. + kubeClientCopy.QPS = 2 + kubeClientCopy.Burst = 2 + kubeClient, err := kubernetes.NewForConfig(kubeClientCopy) + if err != nil { + return nil, err + } + + selector := labels.NewSelector() + labelsRequirement, err := labels.NewRequirement(psapi.EnforceLevelLabel, selection.DoesNotExist, []string{}) + if err != nil { + return nil, err + } + + c := &PodSecurityReadinessController{ + operatorClient: operatorClient, + kubeClient: kubeClient, + warningsHandler: warningsHandler, + namespaceSelector: selector.Add(*labelsRequirement).String(), + } + + return factory.New(). + WithSync(c.sync). + ResyncEvery(checkInterval). + ToController("PodSecurityReadinessController", recorder), nil +} + +func (c *PodSecurityReadinessController) sync(ctx context.Context, syncCtx factory.SyncContext) error { + nsList, err := c.kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: c.namespaceSelector}) + if err != nil { + return err + } + + conditions := podSecurityOperatorConditions{} + for _, ns := range nsList.Items { + err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + isViolating, err := c.isNamespaceViolating(ctx, &ns) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + if isViolating { + conditions.addViolation(ns.Name) + } + + return nil + }) + if err != nil { + klog.V(2).ErrorS(err, "namespace:", ns.Name) + + // We don't want to sync more often than the resync interval. + return nil + + } + } + + // We expect the Cluster's status conditions to be picked up by the status + // controller and push it into the ClusterOperator's status, where it will + // be evaluated by the ClusterFleetMechanic. + _, _, err = v1helpers.UpdateStatus(ctx, c.operatorClient, conditions.toConditionFuncs()...) + return err +} + +func (c *PodSecurityReadinessController) isNamespaceViolating(ctx context.Context, ns *corev1.Namespace) (bool, error) { + if ns.Labels[psapi.EnforceLevelLabel] != "" { + // If someone has taken care of the enforce label, we don't need to + // check for violations. Global Config nor PS-Label-Syncer will modify + // it. + return false, nil + } + + targetLevel := "" + for _, label := range podSecurityAlertLabels { + levelStr, ok := ns.Labels[label] + if !ok { + continue + } + + level, err := psapi.ParseLevel(levelStr) + if err != nil { + klog.V(4).InfoS("invalid level", "namespace", ns.Name, "level", levelStr) + continue + } + + if targetLevel == "" { + targetLevel = levelStr + continue + } + + if psapi.CompareLevels(psapi.Level(targetLevel), level) < 0 { + targetLevel = levelStr + } + } + + if targetLevel == "" { + // Global Config will set it to "restricted". + targetLevel = string(psapi.LevelRestricted) + } + + nsApply := applyconfiguration.Namespace(ns.Name).WithLabels(map[string]string{ + psapi.EnforceLevelLabel: string(targetLevel), + }) + + _, err := c.kubeClient.CoreV1(). + Namespaces(). + Apply(ctx, nsApply, metav1.ApplyOptions{ + DryRun: []string{metav1.DryRunAll}, + FieldManager: "pod-security-readiness-controller", + }) + if err != nil { + return false, err + } + + // The information we want is in the warnings. It collects violations. + warnings := c.warningsHandler.PopAll() + + return len(warnings) > 0, nil +} diff --git a/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go new file mode 100644 index 0000000000..e74142766a --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/podsecurityreadinesscontroller_test.go @@ -0,0 +1,191 @@ +package podsecurityreadinesscontroller + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" + psapi "k8s.io/pod-security-admission/api" +) + +func TestPodSecurityViolationController(t *testing.T) { + for _, tt := range []struct { + name string + + warnings []string + namespace *corev1.Namespace + + expectedViolation bool + expectedEnforceLabel string + }{ + { + name: "violating against restricted namespace", + warnings: []string{ + "existing pods in namespace \"violating-namespace\" violate the new PodSecurity enforce level \"restricted:latest\"", + "violating-pod: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile", + }, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.AuditLevelLabel: "restricted", + psapi.WarnLevelLabel: "restricted", + }, + }, + }, + expectedViolation: true, + expectedEnforceLabel: "restricted", + }, + { + name: "violating against baseline namespace", + warnings: []string{ + "existing pods in namespace \"violating-namespace\" violate the new PodSecurity enforce level \"restricted:latest\"", + "violating-pod: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile", + }, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.AuditLevelLabel: "baseline", + psapi.WarnLevelLabel: "baseline", + }, + }, + }, + expectedViolation: true, + expectedEnforceLabel: "baseline", + }, + { + name: "non-violating against privileged namespace", + warnings: []string{}, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.AuditLevelLabel: "privileged", + psapi.WarnLevelLabel: "privileged", + }, + }, + }, + expectedEnforceLabel: "privileged", + }, + { + name: "violating against unset namespace", + warnings: []string{ + "existing pods in namespace \"violating-namespace\" violate the new PodSecurity enforce level \"restricted:latest\"", + "violating-pod: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile", + }, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + }, + }, + expectedViolation: true, + expectedEnforceLabel: "restricted", + }, + { + name: "violating against mixed alert labels namespace", + warnings: []string{ + "existing pods in namespace \"violating-namespace\" violate the new PodSecurity enforce level \"restricted:latest\"", + "violating-pod: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile", + }, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.AuditLevelLabel: "privileged", + psapi.WarnLevelLabel: "restricted", + }, + }, + }, + expectedViolation: true, + expectedEnforceLabel: "restricted", + }, + { + name: "non-violating against enforced namespace", + warnings: []string{}, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.EnforceLevelLabel: "privileged", + }, + }, + }, + expectedViolation: false, + expectedEnforceLabel: "", + }, + { + name: "violating against enforced namespace", + warnings: []string{ + "existing pods in namespace \"violating-namespace\" violate the new PodSecurity enforce level \"restricted:latest\"", + "violating-pod: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile", + }, + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "violating-namespace", + Labels: map[string]string{ + psapi.EnforceLevelLabel: "privileged", + }, + }, + }, + expectedViolation: false, + expectedEnforceLabel: "", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewSimpleClientset() + fakeClient.PrependReactor("patch", "namespaces", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + patchAction, ok := action.(clienttesting.PatchAction) + if !ok { + return false, nil, fmt.Errorf("invalid action type") + } + + patchBytes := patchAction.GetPatch() + patchMap := make(map[string]interface{}) + if err := json.Unmarshal(patchBytes, &patchMap); err != nil { + return false, nil, fmt.Errorf("failed to unmarshal patch: %v", err) + } + + metadata, ok := patchMap["metadata"].(map[string]interface{}) + if !ok { + return false, nil, fmt.Errorf("patch does not contain metadata") + } + + labels, ok := metadata["labels"].(map[string]interface{}) + if !ok { + return false, nil, fmt.Errorf("patch does not contain labels") + } + + // Check if the expected label is set correctly + if labels[psapi.EnforceLevelLabel] != tt.expectedEnforceLabel { + return false, nil, fmt.Errorf("expected enforce label %s, got %s", tt.expectedEnforceLabel, labels[psapi.EnforceLevelLabel]) + } + + return true, nil, nil + }) + + controller := &PodSecurityReadinessController{ + kubeClient: fakeClient, + warningsHandler: &warningsHandler{ + warnings: tt.warnings, + }, + } + + isViolating, err := controller.isNamespaceViolating(context.TODO(), tt.namespace) + if err != nil { + t.Error(err) + } + + if isViolating != tt.expectedViolation { + t.Errorf("expected violation %v, got %v", tt.expectedViolation, isViolating) + } + }) + } +} diff --git a/pkg/operator/podsecurityreadinesscontroller/warning.go b/pkg/operator/podsecurityreadinesscontroller/warning.go new file mode 100644 index 0000000000..1b161cc0c7 --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/warning.go @@ -0,0 +1,24 @@ +package podsecurityreadinesscontroller + +// warningsHandler collects the warnings and makes them available. +type warningsHandler struct { + warnings []string +} + +// HandleWarningHeader implements the WarningHandler interface. It stores the +// warning headers. +func (w *warningsHandler) HandleWarningHeader(code int, agent string, text string) { + if text == "" { + return + } + + w.warnings = append(w.warnings, text) +} + +// PopAll returns all warnings and clears the slice. +func (w *warningsHandler) PopAll() []string { + warnings := w.warnings + w.warnings = []string{} + + return warnings +} diff --git a/pkg/operator/podsecurityreadinesscontroller/warning_test.go b/pkg/operator/podsecurityreadinesscontroller/warning_test.go new file mode 100644 index 0000000000..447aac986c --- /dev/null +++ b/pkg/operator/podsecurityreadinesscontroller/warning_test.go @@ -0,0 +1,21 @@ +package podsecurityreadinesscontroller + +import ( + "strings" + "testing" +) + +func TestWarningHandler(t *testing.T) { + w := warningsHandler{} + warningMessage := "warning" + w.HandleWarningHeader(0, "", warningMessage) + + actualWarnings := w.PopAll() + if strings.Compare(actualWarnings[0], warningMessage) != 0 { + t.Errorf("Expected warning to be %q, got %q", warningMessage, actualWarnings[0]) + } + + if len(w.PopAll()) != 0 { + t.Error("Expected PopAll to return an empty slice") + } +} diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index 9b0309a502..09bca570bc 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -27,6 +27,7 @@ import ( "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/kubeletversionskewcontroller" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/nodekubeconfigcontroller" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/operatorclient" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/podsecurityreadinesscontroller" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/resourcesynccontroller" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/serviceaccountissuercontroller" "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/startupmonitorreadiness" @@ -432,6 +433,15 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle controllerContext.EventRecorder, ) + podSecurityReadinessController, err := podsecurityreadinesscontroller.NewPodSecurityReadinessController( + controllerContext.ProtoKubeConfig, + operatorClient, + controllerContext.EventRecorder, + ) + if err != nil { + return err + } + // register termination metrics terminationobserver.RegisterMetrics() @@ -466,6 +476,7 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle go latencyProfileController.Run(ctx, 1) go webhookSupportabilityController.Run(ctx, 1) go serviceAccountIssuerController.Run(ctx, 1) + go podSecurityReadinessController.Run(ctx, 1) <-ctx.Done() return nil diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/rbac.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/rbac.go index 0e378edd2d..38a92222ed 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/rbac.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/rbac.go @@ -2,7 +2,6 @@ package resourceapply import ( "context" - "fmt" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -15,12 +14,8 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" ) -// ApplyClusterRole merges objectmeta, requires rules, aggregation rules are not allowed for now. +// ApplyClusterRole merges objectmeta, requires rules. func ApplyClusterRole(ctx context.Context, client rbacclientv1.ClusterRolesGetter, recorder events.Recorder, required *rbacv1.ClusterRole) (*rbacv1.ClusterRole, bool, error) { - if required.AggregationRule != nil && len(required.AggregationRule.ClusterRoleSelectors) != 0 { - return nil, false, fmt.Errorf("cannot create an aggregated cluster role") - } - existing, err := client.ClusterRoles().Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() @@ -37,13 +32,23 @@ func ApplyClusterRole(ctx context.Context, client rbacclientv1.ClusterRolesGette existingCopy := existing.DeepCopy() resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) - contentSame := equality.Semantic.DeepEqual(existingCopy.Rules, required.Rules) - if contentSame && !*modified { + rulesContentSame := equality.Semantic.DeepEqual(existingCopy.Rules, required.Rules) + aggregationRuleContentSame := equality.Semantic.DeepEqual(existingCopy.AggregationRule, required.AggregationRule) + + if aggregationRuleContentSame && rulesContentSame && !*modified { return existingCopy, false, nil } - existingCopy.Rules = required.Rules - existingCopy.AggregationRule = nil + if !aggregationRuleContentSame { + existingCopy.AggregationRule = required.AggregationRule + } + + // The control plane controller that reconciles ClusterRoles + // overwrites any values that are manually specified in the rules field of an aggregate ClusterRole. + // As such skip reconciling on the Rules field when the AggregationRule is set. + if !rulesContentSame && required.AggregationRule == nil { + existingCopy.Rules = required.Rules + } if klog.V(4).Enabled() { klog.Infof("ClusterRole %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy)) diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go b/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go index af361317ed..56e1496b85 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/condition.go @@ -89,8 +89,8 @@ func UnionCondition(conditionType string, defaultConditionStatus operatorv1.Cond // on true, but Available merges on false. Thing of it like an anti-default. // // If inertia is non-nil, then resist returning a condition with a status opposite the defaultConditionStatus. -func UnionClusterCondition(conditionType string, defaultConditionStatus operatorv1.ConditionStatus, inertia Inertia, allConditions ...operatorv1.OperatorCondition) configv1.ClusterOperatorStatusCondition { - cnd := UnionCondition(conditionType, defaultConditionStatus, inertia, allConditions...) +func UnionClusterCondition(conditionType configv1.ClusterStatusConditionType, defaultConditionStatus operatorv1.ConditionStatus, inertia Inertia, allConditions ...operatorv1.OperatorCondition) configv1.ClusterOperatorStatusCondition { + cnd := UnionCondition(string(conditionType), defaultConditionStatus, inertia, allConditions...) return OperatorConditionToClusterOperatorCondition(cnd) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go index f0d74f8c58..23a4965fdf 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/status/status_controller.go @@ -164,6 +164,7 @@ func (c StatusSyncer) Sync(ctx context.Context, syncCtx factory.SyncContext) err configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorProgressing, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}) configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorDegraded, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}) configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.OperatorUpgradeable, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, configv1.ClusterOperatorStatusCondition{Type: configv1.EvaluationConditionsDetected, Status: configv1.ConditionUnknown, Reason: "Unmanaged"}) if equality.Semantic.DeepEqual(clusterOperatorObj, originalClusterOperatorObj) { return nil @@ -199,10 +200,11 @@ func (c StatusSyncer) Sync(ctx context.Context, syncCtx factory.SyncContext) err clusterOperatorObj.Status.RelatedObjects = c.relatedObjects } - configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition("Degraded", operatorv1.ConditionFalse, c.degradedInertia, currentDetailedStatus.Conditions...)) - configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition("Progressing", operatorv1.ConditionFalse, nil, currentDetailedStatus.Conditions...)) - configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition("Available", operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...)) - configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition("Upgradeable", operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...)) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorDegraded, operatorv1.ConditionFalse, c.degradedInertia, currentDetailedStatus.Conditions...)) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorProgressing, operatorv1.ConditionFalse, nil, currentDetailedStatus.Conditions...)) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorAvailable, operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...)) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.OperatorUpgradeable, operatorv1.ConditionTrue, nil, currentDetailedStatus.Conditions...)) + configv1helpers.SetStatusCondition(&clusterOperatorObj.Status.Conditions, UnionClusterCondition(configv1.EvaluationConditionsDetected, operatorv1.ConditionFalse, nil, currentDetailedStatus.Conditions...)) c.syncStatusVersions(clusterOperatorObj, syncCtx) diff --git a/vendor/k8s.io/pod-security-admission/LICENSE b/vendor/k8s.io/pod-security-admission/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/k8s.io/pod-security-admission/api/attributes.go b/vendor/k8s.io/pod-security-admission/api/attributes.go new file mode 100644 index 0000000000..ce6c16fbb6 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/api/attributes.go @@ -0,0 +1,146 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Attributes exposes the admission request parameters consumed by the PodSecurity admission controller. +type Attributes interface { + // GetName is the name of the object associated with the request. + GetName() string + // GetNamespace is the namespace associated with the request (if any) + GetNamespace() string + // GetResource is the name of the resource being requested. This is not the kind. For example: pods + GetResource() schema.GroupVersionResource + // GetKind is the name of the kind being requested. For example: Pod + GetKind() schema.GroupVersionKind + // GetSubresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind. + // For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod" + // (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding". + GetSubresource() string + // GetOperation is the operation being performed + GetOperation() admissionv1.Operation + + // GetObject returns the typed Object from incoming request. + // For objects in the core API group, the result must use the v1 API. + GetObject() (runtime.Object, error) + // GetOldObject returns the typed existing object. Only populated for UPDATE requests. + // For objects in the core API group, the result must use the v1 API. + GetOldObject() (runtime.Object, error) + // GetUserName is the requesting user's authenticated name. + GetUserName() string +} + +// AttributesRecord is a simple struct implementing the Attributes interface. +type AttributesRecord struct { + Name string + Namespace string + Kind schema.GroupVersionKind + Resource schema.GroupVersionResource + Subresource string + Operation admissionv1.Operation + Object runtime.Object + OldObject runtime.Object + Username string +} + +func (a *AttributesRecord) GetName() string { + return a.Name +} +func (a *AttributesRecord) GetNamespace() string { + return a.Namespace +} +func (a *AttributesRecord) GetKind() schema.GroupVersionKind { + return a.Kind +} +func (a *AttributesRecord) GetResource() schema.GroupVersionResource { + return a.Resource +} +func (a *AttributesRecord) GetSubresource() string { + return a.Subresource +} +func (a *AttributesRecord) GetOperation() admissionv1.Operation { + return a.Operation +} +func (a *AttributesRecord) GetUserName() string { + return a.Username +} +func (a *AttributesRecord) GetObject() (runtime.Object, error) { + return a.Object, nil +} +func (a *AttributesRecord) GetOldObject() (runtime.Object, error) { + return a.OldObject, nil +} + +var _ Attributes = &AttributesRecord{} + +// RequestAttributes adapts an admission.Request to the Attributes interface. +func RequestAttributes(request *admissionv1.AdmissionRequest, decoder runtime.Decoder) Attributes { + return &attributes{ + r: request, + decoder: decoder, + } +} + +// attributes is an interface used by AdmissionController to get information about a request +// that is used to make an admission decision. +type attributes struct { + r *admissionv1.AdmissionRequest + decoder runtime.Decoder +} + +func (a *attributes) GetName() string { + return a.r.Name +} +func (a *attributes) GetNamespace() string { + return a.r.Namespace +} +func (a *attributes) GetKind() schema.GroupVersionKind { + return schema.GroupVersionKind(a.r.Kind) +} +func (a *attributes) GetResource() schema.GroupVersionResource { + return schema.GroupVersionResource(a.r.Resource) +} +func (a *attributes) GetSubresource() string { + return a.r.RequestSubResource +} +func (a *attributes) GetOperation() admissionv1.Operation { + return a.r.Operation +} +func (a *attributes) GetUserName() string { + return a.r.UserInfo.Username +} +func (a *attributes) GetObject() (runtime.Object, error) { + return a.decode(a.r.Object) +} +func (a *attributes) GetOldObject() (runtime.Object, error) { + return a.decode(a.r.OldObject) +} +func (a *attributes) decode(in runtime.RawExtension) (runtime.Object, error) { + if in.Raw == nil { + return nil, nil + } + gvk := schema.GroupVersionKind(a.r.Kind) + out, _, err := a.decoder.Decode(in.Raw, &gvk, nil) + return out, err +} + +var _ Attributes = &attributes{} diff --git a/vendor/k8s.io/pod-security-admission/api/constants.go b/vendor/k8s.io/pod-security-admission/api/constants.go new file mode 100644 index 0000000000..9d87ad59b1 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/api/constants.go @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +type Level string + +const ( + LevelPrivileged Level = "privileged" + LevelBaseline Level = "baseline" + LevelRestricted Level = "restricted" +) + +var validLevels = []string{ + string(LevelPrivileged), + string(LevelBaseline), + string(LevelRestricted), +} + +const VersionLatest = "latest" + +const AuditAnnotationPrefix = labelPrefix + +const ( + labelPrefix = "pod-security.kubernetes.io/" + + EnforceLevelLabel = labelPrefix + "enforce" + EnforceVersionLabel = labelPrefix + "enforce-version" + AuditLevelLabel = labelPrefix + "audit" + AuditVersionLabel = labelPrefix + "audit-version" + WarnLevelLabel = labelPrefix + "warn" + WarnVersionLabel = labelPrefix + "warn-version" + + ExemptionReasonAnnotationKey = "exempt" + AuditViolationsAnnotationKey = "audit-violations" + EnforcedPolicyAnnotationKey = "enforce-policy" +) diff --git a/vendor/k8s.io/pod-security-admission/api/doc.go b/vendor/k8s.io/pod-security-admission/api/doc.go new file mode 100644 index 0000000000..a35374be3a --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/api/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package api contains constants and helpers for PodSecurity admission label keys and values +package api // import "k8s.io/pod-security-admission/api" diff --git a/vendor/k8s.io/pod-security-admission/api/helpers.go b/vendor/k8s.io/pod-security-admission/api/helpers.go new file mode 100644 index 0000000000..706b6b36b2 --- /dev/null +++ b/vendor/k8s.io/pod-security-admission/api/helpers.go @@ -0,0 +1,269 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "unicode" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/component-base/version" +) + +type Version struct { + major int + minor int + latest bool +} + +func (v Version) String() string { + if v.latest { + return "latest" + } + return fmt.Sprintf("v%d.%d", v.major, v.minor) +} + +// Older returns true if this version v is older than the other. +func (v *Version) Older(other Version) bool { + if v.latest { // Latest is always consider newer, even than future versions. + return false + } + if other.latest { + return true + } + if v.major != other.major { + return v.major < other.major + } + return v.minor < other.minor +} + +func (v *Version) Major() int { + return v.major +} +func (v *Version) Minor() int { + return v.minor +} +func (v *Version) Latest() bool { + return v.latest +} + +func MajorMinorVersion(major, minor int) Version { + return Version{major: major, minor: minor} +} + +// GetAPIVersion get the version of apiServer and return the version major and minor +func GetAPIVersion() Version { + var err error + v := Version{} + apiVersion := version.Get() + major, err := strconv.Atoi(apiVersion.Major) + if err != nil { + return v + } + // split the "normal" + and - for semver stuff to get the leading minor number + minorString := strings.FieldsFunc(apiVersion.Minor, func(r rune) bool { + return !unicode.IsDigit(r) + })[0] + minor, err := strconv.Atoi(minorString) + if err != nil { + return v + } + v = MajorMinorVersion(major, minor) + return v +} + +func LatestVersion() Version { + return Version{latest: true} +} + +// ParseLevel returns the level that should be evaluated. +// level must be "privileged", "baseline", or "restricted". +// if level does not match one of those strings, "restricted" and an error is returned. +func ParseLevel(level string) (Level, error) { + switch Level(level) { + case LevelPrivileged, LevelBaseline, LevelRestricted: + return Level(level), nil + default: + return LevelRestricted, fmt.Errorf(`must be one of %s`, strings.Join(validLevels, ", ")) + } +} + +// Valid checks whether the level l is a valid level. +func (l *Level) Valid() bool { + switch *l { + case LevelPrivileged, LevelBaseline, LevelRestricted: + return true + default: + return false + } +} + +var versionRegexp = regexp.MustCompile(`^v1\.([0-9]|[1-9][0-9]*)$`) + +// ParseVersion returns the policy version that should be evaluated. +// version must be "latest" or "v1.x". +// If version does not match one of those patterns, the latest version and an error is returned. +func ParseVersion(version string) (Version, error) { + if version == "latest" { + return Version{latest: true}, nil + } + match := versionRegexp.FindStringSubmatch(version) + if len(match) != 2 { + return Version{latest: true}, fmt.Errorf(`must be "latest" or "v1.x"`) + } + versionNumber, err := strconv.Atoi(match[1]) + if err != nil || versionNumber < 0 { + return Version{latest: true}, fmt.Errorf(`must be "latest" or "v1.x"`) + } + return Version{major: 1, minor: versionNumber}, nil +} + +type LevelVersion struct { + Level + Version +} + +func (lv LevelVersion) String() string { + return fmt.Sprintf("%s:%s", lv.Level, lv.Version) +} + +// Equivalent determines whether two LevelVersions are functionally equivalent. LevelVersions are +// considered equivalent if both are privileged, or both levels & versions are equal. +func (lv *LevelVersion) Equivalent(other *LevelVersion) bool { + return (lv.Level == LevelPrivileged && other.Level == LevelPrivileged) || + (lv.Level == other.Level && lv.Version == other.Version) +} + +type Policy struct { + Enforce LevelVersion + Audit LevelVersion + Warn LevelVersion +} + +func (p *Policy) String() string { + return fmt.Sprintf("enforce=%#v, audit=%#v, warn=%#v", p.Enforce, p.Audit, p.Warn) +} + +// Equivalent determines whether two policies are functionally equivalent. Policies are considered +// equivalent if all 3 modes are considered equivalent. +func (p *Policy) Equivalent(other *Policy) bool { + return p.Enforce.Equivalent(&other.Enforce) && p.Audit.Equivalent(&other.Audit) && p.Warn.Equivalent(&other.Warn) +} + +// FullyPrivileged returns true if all 3 policy modes are privileged. +func (p *Policy) FullyPrivileged() bool { + return p.Enforce.Level == LevelPrivileged && + p.Audit.Level == LevelPrivileged && + p.Warn.Level == LevelPrivileged +} + +// PolicyToEvaluate resolves the PodSecurity namespace labels to the policy for that namespace, +// falling back to the provided defaults when a label is unspecified. A valid policy is always +// returned, even when an error is returned. If labels cannot be parsed correctly, the values of +// "restricted" and "latest" are used for level and version respectively. +func PolicyToEvaluate(labels map[string]string, defaults Policy) (Policy, field.ErrorList) { + var ( + err error + errs field.ErrorList + + p = defaults + + hasEnforceLevel bool + hasWarnLevel, hasWarnVersion bool + ) + if len(labels) == 0 { + return p, nil + } + if level, ok := labels[EnforceLevelLabel]; ok { + p.Enforce.Level, err = ParseLevel(level) + hasEnforceLevel = (err == nil) // Don't default warn in case of error + errs = appendErr(errs, err, EnforceLevelLabel, level) + } + if version, ok := labels[EnforceVersionLabel]; ok { + p.Enforce.Version, err = ParseVersion(version) + errs = appendErr(errs, err, EnforceVersionLabel, version) + } + if level, ok := labels[AuditLevelLabel]; ok { + p.Audit.Level, err = ParseLevel(level) + errs = appendErr(errs, err, AuditLevelLabel, level) + if err != nil { + p.Audit.Level = LevelPrivileged // Fail open for audit. + } + } + if version, ok := labels[AuditVersionLabel]; ok { + p.Audit.Version, err = ParseVersion(version) + errs = appendErr(errs, err, AuditVersionLabel, version) + } + if level, ok := labels[WarnLevelLabel]; ok { + hasWarnLevel = true + p.Warn.Level, err = ParseLevel(level) + errs = appendErr(errs, err, WarnLevelLabel, level) + if err != nil { + p.Warn.Level = LevelPrivileged // Fail open for warn. + } + } + if version, ok := labels[WarnVersionLabel]; ok { + hasWarnVersion = true + p.Warn.Version, err = ParseVersion(version) + errs = appendErr(errs, err, WarnVersionLabel, version) + } + + // Default warn to the enforce level when explicitly set to a more restrictive level. + if !hasWarnLevel && hasEnforceLevel && CompareLevels(p.Enforce.Level, p.Warn.Level) > 0 { + p.Warn.Level = p.Enforce.Level + if !hasWarnVersion { + p.Warn.Version = p.Enforce.Version + } + } + + return p, errs +} + +// CompareLevels returns an integer comparing two levels by strictness. The result will be 0 if +// a==b, -1 if a is less strict than b, and +1 if a is more strict than b. +func CompareLevels(a, b Level) int { + if a == b { + return 0 + } + switch a { + case LevelPrivileged: + return -1 + case LevelRestricted: + return 1 + default: + if b == LevelPrivileged { + return 1 + } else if b == LevelRestricted { + return -1 + } + } + // This should only happen if both a & b are invalid levels. + return 0 +} + +var labelsPath = field.NewPath("metadata", "labels") + +// appendErr is a helper function to collect label-specific errors. +func appendErr(errs field.ErrorList, err error, label, value string) field.ErrorList { + if err != nil { + return append(errs, field.Invalid(labelsPath.Key(label), value, err.Error())) + } + return errs +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e0611124fb..0c3cb62246 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -330,7 +330,7 @@ github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/i github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane github.com/openshift/client-go/operatorcontrolplane/informers/externalversions/operatorcontrolplane/v1alpha1 github.com/openshift/client-go/operatorcontrolplane/listers/operatorcontrolplane/v1alpha1 -# github.com/openshift/library-go v0.0.0-20231103161458-0ec67489d123 +# github.com/openshift/library-go v0.0.0-20231213084759-840298df1eee ## explicit; go 1.20 github.com/openshift/library-go/pkg/assets github.com/openshift/library-go/pkg/authorization/hardcodedauthorizer @@ -1382,6 +1382,9 @@ k8s.io/kube-openapi/pkg/validation/errors k8s.io/kube-openapi/pkg/validation/spec k8s.io/kube-openapi/pkg/validation/strfmt k8s.io/kube-openapi/pkg/validation/strfmt/bson +# k8s.io/pod-security-admission v0.27.4 +## explicit; go 1.20 +k8s.io/pod-security-admission/api # k8s.io/utils v0.0.0-20230726121419-3b25d923346b ## explicit; go 1.18 k8s.io/utils/buffer