From 8de04965f8712f0b04573be012a17b5e02ff2f27 Mon Sep 17 00:00:00 2001 From: Ricardo Weir Date: Thu, 19 Aug 2021 16:56:12 -0700 Subject: [PATCH 1/2] Add feature type --- pkg/codegen/main.go | 1 + .../management.cattle.io/v3/objects.go | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/pkg/codegen/main.go b/pkg/codegen/main.go index 9f898b7f7..eb36a5c0f 100644 --- a/pkg/codegen/main.go +++ b/pkg/codegen/main.go @@ -42,6 +42,7 @@ func main() { Types: []interface{}{ &v3.Cluster{}, &v3.ClusterRoleTemplateBinding{}, + &v3.Feature{}, &v3.FleetWorkspace{}, &v3.GlobalRole{}, &v3.GlobalRoleBinding{}, diff --git a/pkg/generated/objects/management.cattle.io/v3/objects.go b/pkg/generated/objects/management.cattle.io/v3/objects.go index a9a6d0821..31c8f51bc 100644 --- a/pkg/generated/objects/management.cattle.io/v3/objects.go +++ b/pkg/generated/objects/management.cattle.io/v3/objects.go @@ -99,6 +99,52 @@ func ClusterRoleTemplateBindingFromRequest(request *webhook.Request) (*v3.Cluste return object.(*v3.ClusterRoleTemplateBinding), nil } +// FeatureOldAndNewFromRequest gets the old and new Feature objects, respectively, from the webhook request. +// If the request is a Delete operation, then the new object is the zero value for Feature. +// Similarly, if the request is a Create operation, then the old object is the zero value for Feature. +func FeatureOldAndNewFromRequest(request *webhook.Request) (*v3.Feature, *v3.Feature, error) { + var object runtime.Object + var err error + if request.Operation != admissionv1.Delete { + object, err = request.DecodeObject() + if err != nil { + return nil, nil, err + } + } else { + object = &v3.Feature{} + } + + if request.Operation == admissionv1.Create { + return &v3.Feature{}, object.(*v3.Feature), nil + } + + oldObject, err := request.DecodeOldObject() + if err != nil { + return nil, nil, err + } + + return oldObject.(*v3.Feature), object.(*v3.Feature), nil +} + +// FeatureFromRequest returns a Feature object from the webhook request. +// If the operation is a Delete operation, then the old object is returned. +// Otherwise, the new object is returned. +func FeatureFromRequest(request *webhook.Request) (*v3.Feature, error) { + var object runtime.Object + var err error + if request.Operation == admissionv1.Delete { + object, err = request.DecodeOldObject() + } else { + object, err = request.DecodeObject() + } + + if err != nil { + return nil, err + } + + return object.(*v3.Feature), nil +} + // FleetWorkspaceOldAndNewFromRequest gets the old and new FleetWorkspace objects, respectively, from the webhook request. // If the request is a Delete operation, then the new object is the zero value for FleetWorkspace. // Similarly, if the request is a Create operation, then the old object is the zero value for FleetWorkspace. From c563c16d19d8462bc2b65e13b9bff9a4a8e9af74 Mon Sep 17 00:00:00 2001 From: Ricardo Weir Date: Thu, 19 Aug 2021 17:36:59 -0700 Subject: [PATCH 2/2] Migrate Feature type validation --- pkg/resources/validation/feature/feature.go | 64 +++++++++++++++++++++ pkg/server/server.go | 11 ++++ pkg/server/validation.go | 3 + 3 files changed, 78 insertions(+) create mode 100644 pkg/resources/validation/feature/feature.go diff --git a/pkg/resources/validation/feature/feature.go b/pkg/resources/validation/feature/feature.go new file mode 100644 index 000000000..6c3085941 --- /dev/null +++ b/pkg/resources/validation/feature/feature.go @@ -0,0 +1,64 @@ +package feature + +import ( + "fmt" + "net/http" + "time" + + objectsv3 "github.com/rancher/webhook/pkg/generated/objects/management.cattle.io/v3" + "github.com/rancher/wrangler/pkg/webhook" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/trace" +) + +func NewValidator() webhook.Handler { + return &featureValidator{} +} + +type featureValidator struct{} + +func (fv *featureValidator) Admit(response *webhook.Response, request *webhook.Request) error { + listTrace := trace.New("featureValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username}) + defer listTrace.LogIfLong(2 * time.Second) + + oldFeature, newFeature, err := objectsv3.FeatureOldAndNewFromRequest(request) + if err != nil { + return err + } + + if !isValidFeatureValue(newFeature.Status.LockedValue, oldFeature.Spec.Value, newFeature.Spec.Value) { + response.Result = &metav1.Status{ + Status: "Failure", + Message: fmt.Sprintf("feature flag cannot be changed from current value: %v", *newFeature.Status.LockedValue), + Reason: metav1.StatusReasonInvalid, + Code: http.StatusBadRequest, + } + response.Allowed = false + return nil + } + + response.Allowed = true + return nil +} + +// isValidFeatureValue checks that desired value does not change value on spec unless lockedValue +// is nil or it is equal to the lockedValue. +func isValidFeatureValue(lockedValue *bool, oldSpecValue *bool, desiredSpecValue *bool) bool { + if lockedValue == nil { + return true + } + + if oldSpecValue == nil && desiredSpecValue == nil { + return true + } + + if oldSpecValue != nil && desiredSpecValue != nil && *oldSpecValue == *desiredSpecValue { + return true + } + + if desiredSpecValue != nil && *desiredSpecValue == *lockedValue { + return true + } + + return false +} diff --git a/pkg/server/server.go b/pkg/server/server.go index dcf3fb8e0..a7d211fef 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -207,6 +207,17 @@ func listenAndServe(ctx context.Context, clients *clients.Clients, handler http. Scope: &clusterScope, }, }, + { + Operations: []v1.OperationType{ + v1.Update, + }, + Rule: v1.Rule{ + APIGroups: []string{"management.cattle.io"}, + APIVersions: []string{"v3"}, + Resources: []string{"features"}, + Scope: &clusterScope, + }, + }, { Operations: []v1.OperationType{ v1.Create, diff --git a/pkg/server/validation.go b/pkg/server/validation.go index c496175a7..00ed23c43 100644 --- a/pkg/server/validation.go +++ b/pkg/server/validation.go @@ -10,6 +10,7 @@ import ( "github.com/rancher/webhook/pkg/clients" "github.com/rancher/webhook/pkg/resources/validation/cluster" "github.com/rancher/webhook/pkg/resources/validation/clusterroletemplatebinding" + "github.com/rancher/webhook/pkg/resources/validation/feature" "github.com/rancher/webhook/pkg/resources/validation/globalrole" "github.com/rancher/webhook/pkg/resources/validation/globalrolebinding" "github.com/rancher/webhook/pkg/resources/validation/projectroletemplatebinding" @@ -27,6 +28,7 @@ func Validation(clients *clients.Clients) (http.Handler, error) { globalRoles := globalrole.NewValidator() prtbs := projectroletemplatebinding.NewValidator(clients.Management.RoleTemplate().Cache(), clients.EscalationChecker) crtbs := clusterroletemplatebinding.NewValidator(clients.Management.RoleTemplate().Cache(), clients.EscalationChecker) + f := feature.NewValidator() roleTemplates := roletemplate.NewValidator(clients.EscalationChecker) provisioningCluster := cluster.NewProvisioningClusterValidator(clients.K8s.AuthorizationV1().SubjectAccessReviews()) @@ -35,6 +37,7 @@ func Validation(clients *clients.Clients) (http.Handler, error) { router.Kind("GlobalRole").Group(management.GroupName).Type(&v3.GlobalRole{}).Handle(globalRoles) router.Kind("ClusterRoleTemplateBinding").Group(management.GroupName).Type(&v3.ClusterRoleTemplateBinding{}).Handle(crtbs) router.Kind("ProjectRoleTemplateBinding").Group(management.GroupName).Type(&v3.ProjectRoleTemplateBinding{}).Handle(prtbs) + router.Kind("Feature").Group(management.GroupName).Type(&v3.Feature{}).Handle(f) router.Kind("Cluster").Group(provisioning.GroupName).Type(&v1.Cluster{}).Handle(provisioningCluster) }