diff --git a/pkg/apis/rbac/types.go b/pkg/apis/rbac/types.go index a0b2c262457..ddc2456a022 100644 --- a/pkg/apis/rbac/types.go +++ b/pkg/apis/rbac/types.go @@ -34,6 +34,9 @@ const ( GroupKind = "Group" ServiceAccountKind = "ServiceAccount" UserKind = "User" + + // AutoUpdateAnnotationKey is the name of an annotation which prevents reconciliation if set to "false" + AutoUpdateAnnotationKey = "rbac.authorization.kubernetes.io/autoupdate" ) // PolicyRule holds information that describes a policy rule, but does not contain information diff --git a/pkg/apis/rbac/v1alpha1/types.go b/pkg/apis/rbac/v1alpha1/types.go index fb716172c2f..e9f8efb3b47 100644 --- a/pkg/apis/rbac/v1alpha1/types.go +++ b/pkg/apis/rbac/v1alpha1/types.go @@ -34,6 +34,9 @@ const ( GroupKind = "Group" ServiceAccountKind = "ServiceAccount" UserKind = "User" + + // AutoUpdateAnnotationKey is the name of an annotation which prevents reconciliation if set to "false" + AutoUpdateAnnotationKey = "rbac.authorization.kubernetes.io/autoupdate" ) // Authorization is calculated against diff --git a/pkg/apis/rbac/v1beta1/types.go b/pkg/apis/rbac/v1beta1/types.go index ecd2e5628c8..89a47db9e38 100644 --- a/pkg/apis/rbac/v1beta1/types.go +++ b/pkg/apis/rbac/v1beta1/types.go @@ -34,6 +34,9 @@ const ( GroupKind = "Group" ServiceAccountKind = "ServiceAccount" UserKind = "User" + + // AutoUpdateAnnotationKey is the name of an annotation which prevents reconciliation if set to "false" + AutoUpdateAnnotationKey = "rbac.authorization.kubernetes.io/autoupdate" ) // Authorization is calculated against diff --git a/pkg/registry/rbac/BUILD b/pkg/registry/rbac/BUILD index 1a772340d00..19117f1a27c 100644 --- a/pkg/registry/rbac/BUILD +++ b/pkg/registry/rbac/BUILD @@ -33,6 +33,7 @@ filegroup( ":package-srcs", "//pkg/registry/rbac/clusterrole:all-srcs", "//pkg/registry/rbac/clusterrolebinding:all-srcs", + "//pkg/registry/rbac/reconciliation:all-srcs", "//pkg/registry/rbac/rest:all-srcs", "//pkg/registry/rbac/role:all-srcs", "//pkg/registry/rbac/rolebinding:all-srcs", diff --git a/pkg/registry/rbac/reconciliation/BUILD b/pkg/registry/rbac/reconciliation/BUILD new file mode 100644 index 00000000000..b2c9a10d5d5 --- /dev/null +++ b/pkg/registry/rbac/reconciliation/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = [ + "reconcile_clusterrole_test.go", + "reconcile_clusterrolebindings_test.go", + ], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/rbac:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + "reconcile_clusterrole.go", + "reconcile_clusterrolebindings.go", + ], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/apis/rbac:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion:go_default_library", + "//pkg/registry/rbac/validation:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/errors", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/registry/rbac/reconciliation/reconcile_clusterrole.go b/pkg/registry/rbac/reconciliation/reconcile_clusterrole.go new file mode 100644 index 00000000000..5df1b06e02a --- /dev/null +++ b/pkg/registry/rbac/reconciliation/reconcile_clusterrole.go @@ -0,0 +1,200 @@ +/* +Copyright 2017 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 reconciliation + +import ( + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/rbac" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" + "k8s.io/kubernetes/pkg/registry/rbac/validation" +) + +type ReconcileOperation string + +var ( + ReconcileCreate ReconcileOperation = "create" + ReconcileUpdate ReconcileOperation = "update" + ReconcileRecreate ReconcileOperation = "recreate" + ReconcileNone ReconcileOperation = "none" +) + +type ReconcileClusterRoleOptions struct { + // Role is the expected role that will be reconciled + Role *rbac.ClusterRole + // Confirm indicates writes should be performed. When false, results are returned as a dry-run. + Confirm bool + // RemoveExtraPermissions indicates reconciliation should remove extra permissions from an existing role + RemoveExtraPermissions bool + // Client is used to look up existing roles, and create/update the role when Confirm=true + Client internalversion.ClusterRoleInterface +} + +type ReconcileClusterRoleResult struct { + // Role is the reconciled role from the reconciliation operation. + // If the reconcile was performed as a dry-run, or the existing role was protected, the reconciled role is not persisted. + Role *rbac.ClusterRole + + // MissingRules contains expected rules that were missing from the currently persisted role + MissingRules []rbac.PolicyRule + // ExtraRules contains extra permissions the currently persisted role had + ExtraRules []rbac.PolicyRule + + // Operation is the API operation required to reconcile. + // If no reconciliation was needed, it is set to ReconcileNone. + // If options.Confirm == false, the reconcile was in dry-run mode, so the operation was not performed. + // If result.Protected == true, the role opted out of reconciliation, so the operation was not performed. + // Otherwise, the operation was performed. + Operation ReconcileOperation + // Protected indicates an existing role prevented reconciliation + Protected bool +} + +func (o *ReconcileClusterRoleOptions) Run() (*ReconcileClusterRoleResult, error) { + return o.run(0) +} + +func (o *ReconcileClusterRoleOptions) run(attempts int) (*ReconcileClusterRoleResult, error) { + // This keeps us from retrying forever if a role keeps appearing and disappearing as we reconcile. + // Conflict errors on update are handled at a higher level. + if attempts > 2 { + return nil, fmt.Errorf("exceeded maximum attempts") + } + + var result *ReconcileClusterRoleResult + + existing, err := o.Client.Get(o.Role.Name, metav1.GetOptions{}) + switch { + case errors.IsNotFound(err): + result = &ReconcileClusterRoleResult{ + Role: o.Role, + MissingRules: o.Role.Rules, + Operation: ReconcileCreate, + } + + case err != nil: + return nil, err + + default: + result, err = computeReconciledRole(existing, o.Role, o.RemoveExtraPermissions) + if err != nil { + return nil, err + } + } + + // If reconcile-protected, short-circuit + if result.Protected { + return result, nil + } + // If we're in dry-run mode, short-circuit + if !o.Confirm { + return result, nil + } + + switch result.Operation { + case ReconcileCreate: + created, err := o.Client.Create(result.Role) + // If created since we started this reconcile, re-run + if errors.IsAlreadyExists(err) { + return o.run(attempts + 1) + } + if err != nil { + return nil, err + } + result.Role = created + + case ReconcileUpdate: + updated, err := o.Client.Update(result.Role) + // If deleted since we started this reconcile, re-run + if errors.IsNotFound(err) { + return o.run(attempts + 1) + } + if err != nil { + return nil, err + } + result.Role = updated + + case ReconcileNone: + // no-op + + default: + return nil, fmt.Errorf("invalid operation: %v", result.Operation) + } + + return result, nil +} + +// computeReconciledRole returns the role that must be created and/or updated to make the +// existing role's permissions match the expected role's permissions +func computeReconciledRole(existing, expected *rbac.ClusterRole, removeExtraPermissions bool) (*ReconcileClusterRoleResult, error) { + result := &ReconcileClusterRoleResult{Operation: ReconcileNone} + + result.Protected = (existing.Annotations[rbac.AutoUpdateAnnotationKey] == "false") + + // Start with a copy of the existing object + changedObj, err := api.Scheme.DeepCopy(existing) + if err != nil { + return nil, err + } + result.Role = changedObj.(*rbac.ClusterRole) + + // Merge expected annotations and labels + result.Role.Annotations = merge(expected.Annotations, result.Role.Annotations) + if !reflect.DeepEqual(result.Role.Annotations, existing.Annotations) { + result.Operation = ReconcileUpdate + } + result.Role.Labels = merge(expected.Labels, result.Role.Labels) + if !reflect.DeepEqual(result.Role.Labels, existing.Labels) { + result.Operation = ReconcileUpdate + } + + // Compute extra and missing rules + _, result.ExtraRules = validation.Covers(expected.Rules, existing.Rules) + _, result.MissingRules = validation.Covers(existing.Rules, expected.Rules) + + switch { + case !removeExtraPermissions && len(result.MissingRules) > 0: + // add missing rules in the union case + result.Role.Rules = append(result.Role.Rules, result.MissingRules...) + result.Operation = ReconcileUpdate + + case removeExtraPermissions && (len(result.MissingRules) > 0 || len(result.ExtraRules) > 0): + // stomp to expected rules in the non-union case + result.Role.Rules = expected.Rules + result.Operation = ReconcileUpdate + } + + return result, nil +} + +// merge combines the given maps with the later annotations having higher precedence +func merge(maps ...map[string]string) map[string]string { + var output map[string]string = nil + for _, m := range maps { + if m != nil && output == nil { + output = map[string]string{} + } + for k, v := range m { + output[k] = v + } + } + return output +} diff --git a/pkg/registry/rbac/reconciliation/reconcile_clusterrole_test.go b/pkg/registry/rbac/reconciliation/reconcile_clusterrole_test.go new file mode 100644 index 00000000000..f799d409fb9 --- /dev/null +++ b/pkg/registry/rbac/reconciliation/reconcile_clusterrole_test.go @@ -0,0 +1,273 @@ +/* +Copyright 2017 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 reconciliation + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/rbac" +) + +func role(rules []rbac.PolicyRule, labels map[string]string, annotations map[string]string) *rbac.ClusterRole { + return &rbac.ClusterRole{Rules: rules, ObjectMeta: metav1.ObjectMeta{Labels: labels, Annotations: annotations}} +} + +func rules(resources ...string) []rbac.PolicyRule { + r := []rbac.PolicyRule{} + for _, resource := range resources { + r = append(r, rbac.PolicyRule{APIGroups: []string{""}, Verbs: []string{"get"}, Resources: []string{resource}}) + } + return r +} + +type ss map[string]string + +func TestComputeReconciledRole(t *testing.T) { + tests := map[string]struct { + expectedRole *rbac.ClusterRole + actualRole *rbac.ClusterRole + removeExtraPermissions bool + + expectedReconciledRole *rbac.ClusterRole + expectedReconciliationNeeded bool + }{ + "empty": { + expectedRole: role(rules(), nil, nil), + actualRole: role(rules(), nil, nil), + removeExtraPermissions: true, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "match without union": { + expectedRole: role(rules("a"), nil, nil), + actualRole: role(rules("a"), nil, nil), + removeExtraPermissions: true, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "match with union": { + expectedRole: role(rules("a"), nil, nil), + actualRole: role(rules("a"), nil, nil), + removeExtraPermissions: false, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "different rules without union": { + expectedRole: role(rules("a"), nil, nil), + actualRole: role(rules("b"), nil, nil), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), nil, nil), + expectedReconciliationNeeded: true, + }, + "different rules with union": { + expectedRole: role(rules("a"), nil, nil), + actualRole: role(rules("b"), nil, nil), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), nil, nil), + expectedReconciliationNeeded: true, + }, + "match labels without union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("a"), ss{"1": "a"}, nil), + removeExtraPermissions: true, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "match labels with union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("a"), ss{"1": "a"}, nil), + removeExtraPermissions: false, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "different labels without union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("a"), ss{"2": "b"}, nil), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), ss{"1": "a", "2": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "different labels with union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("a"), ss{"2": "b"}, nil), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("a"), ss{"1": "a", "2": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "different labels and rules without union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("b"), ss{"2": "b"}, nil), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), ss{"1": "a", "2": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "different labels and rules with union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("b"), ss{"2": "b"}, nil), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), ss{"1": "a", "2": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "conflicting labels and rules without union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("b"), ss{"1": "b"}, nil), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), ss{"1": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "conflicting labels and rules with union": { + expectedRole: role(rules("a"), ss{"1": "a"}, nil), + actualRole: role(rules("b"), ss{"1": "b"}, nil), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), ss{"1": "b"}, nil), + expectedReconciliationNeeded: true, + }, + "match annotations without union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("a"), nil, ss{"1": "a"}), + removeExtraPermissions: true, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "match annotations with union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("a"), nil, ss{"1": "a"}), + removeExtraPermissions: false, + + expectedReconciledRole: nil, + expectedReconciliationNeeded: false, + }, + "different annotations without union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("a"), nil, ss{"2": "b"}), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), nil, ss{"1": "a", "2": "b"}), + expectedReconciliationNeeded: true, + }, + "different annotations with union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("a"), nil, ss{"2": "b"}), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("a"), nil, ss{"1": "a", "2": "b"}), + expectedReconciliationNeeded: true, + }, + "different annotations and rules without union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("b"), nil, ss{"2": "b"}), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), nil, ss{"1": "a", "2": "b"}), + expectedReconciliationNeeded: true, + }, + "different annotations and rules with union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("b"), nil, ss{"2": "b"}), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), nil, ss{"1": "a", "2": "b"}), + expectedReconciliationNeeded: true, + }, + "conflicting annotations and rules without union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("b"), nil, ss{"1": "b"}), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), nil, ss{"1": "b"}), + expectedReconciliationNeeded: true, + }, + "conflicting annotations and rules with union": { + expectedRole: role(rules("a"), nil, ss{"1": "a"}), + actualRole: role(rules("b"), nil, ss{"1": "b"}), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), nil, ss{"1": "b"}), + expectedReconciliationNeeded: true, + }, + "conflicting labels/annotations and rules without union": { + expectedRole: role(rules("a"), ss{"3": "d"}, ss{"1": "a"}), + actualRole: role(rules("b"), ss{"4": "e"}, ss{"1": "b"}), + removeExtraPermissions: true, + + expectedReconciledRole: role(rules("a"), ss{"3": "d", "4": "e"}, ss{"1": "b"}), + expectedReconciliationNeeded: true, + }, + "conflicting labels/annotations and rules with union": { + expectedRole: role(rules("a"), ss{"3": "d"}, ss{"1": "a"}), + actualRole: role(rules("b"), ss{"4": "e"}, ss{"1": "b"}), + removeExtraPermissions: false, + + expectedReconciledRole: role(rules("b", "a"), ss{"3": "d", "4": "e"}, ss{"1": "b"}), + expectedReconciliationNeeded: true, + }, + "complex labels/annotations and rules without union": { + expectedRole: role(rules("pods", "nodes", "secrets"), ss{"env": "prod", "color": "blue"}, ss{"description": "fancy", "system": "true"}), + actualRole: role(rules("nodes", "images", "projects"), ss{"color": "red", "team": "pm"}, ss{"system": "false", "owner": "admin", "vip": "yes"}), + removeExtraPermissions: true, + + expectedReconciledRole: role( + rules("pods", "nodes", "secrets"), + ss{"env": "prod", "color": "red", "team": "pm"}, + ss{"description": "fancy", "system": "false", "owner": "admin", "vip": "yes"}), + expectedReconciliationNeeded: true, + }, + "complex labels/annotations and rules with union": { + expectedRole: role(rules("pods", "nodes", "secrets"), ss{"env": "prod", "color": "blue", "manager": "randy"}, ss{"description": "fancy", "system": "true", "up": "true"}), + actualRole: role(rules("nodes", "images", "projects"), ss{"color": "red", "team": "pm"}, ss{"system": "false", "owner": "admin", "vip": "yes", "rate": "down"}), + removeExtraPermissions: false, + + expectedReconciledRole: role( + rules("nodes", "images", "projects", "pods", "secrets"), + ss{"env": "prod", "manager": "randy", "color": "red", "team": "pm"}, + ss{"description": "fancy", "system": "false", "owner": "admin", "vip": "yes", "rate": "down", "up": "true"}), + expectedReconciliationNeeded: true, + }, + } + + for k, tc := range tests { + result, err := computeReconciledRole(tc.actualRole, tc.expectedRole, tc.removeExtraPermissions) + if err != nil { + t.Errorf("%s: %v", k, err) + continue + } + reconciliationNeeded := result.Operation != ReconcileNone + if reconciliationNeeded != tc.expectedReconciliationNeeded { + t.Errorf("%s: Expected\n\t%v\ngot\n\t%v", k, tc.expectedReconciliationNeeded, reconciliationNeeded) + continue + } + if reconciliationNeeded && !api.Semantic.DeepEqual(result.Role, tc.expectedReconciledRole) { + t.Errorf("%s: Expected\n\t%#v\ngot\n\t%#v", k, tc.expectedReconciledRole, result.Role) + } + } +} diff --git a/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings.go b/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings.go new file mode 100644 index 00000000000..fb4d406ecc5 --- /dev/null +++ b/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings.go @@ -0,0 +1,234 @@ +/* +Copyright 2017 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 reconciliation + +import ( + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/rbac" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" +) + +// ReconcileClusterRoleBindingOptions holds options for running a role binding reconciliation +type ReconcileClusterRoleBindingOptions struct { + // RoleBinding is the expected rolebinding that will be reconciled + RoleBinding *rbac.ClusterRoleBinding + // Confirm indicates writes should be performed. When false, results are returned as a dry-run. + Confirm bool + // RemoveExtraSubjects indicates reconciliation should remove extra subjects from an existing role binding + RemoveExtraSubjects bool + // Client is used to look up existing rolebindings, and create/update the rolebinding when Confirm=true + Client internalversion.ClusterRoleBindingInterface +} + +// ReconcileClusterRoleBindingResult holds the result of a reconciliation operation. +type ReconcileClusterRoleBindingResult struct { + // RoleBinding is the reconciled rolebinding from the reconciliation operation. + // If the reconcile was performed as a dry-run, or the existing rolebinding was protected, the reconciled rolebinding is not persisted. + RoleBinding *rbac.ClusterRoleBinding + + // MissingSubjects contains expected subjects that were missing from the currently persisted rolebinding + MissingSubjects []rbac.Subject + // ExtraSubjects contains extra subjects the currently persisted rolebinding had + ExtraSubjects []rbac.Subject + + // Operation is the API operation required to reconcile. + // If no reconciliation was needed, it is set to ReconcileNone. + // If options.Confirm == false, the reconcile was in dry-run mode, so the operation was not performed. + // If result.Protected == true, the rolebinding opted out of reconciliation, so the operation was not performed. + // Otherwise, the operation was performed. + Operation ReconcileOperation + // Protected indicates an existing role prevented reconciliation + Protected bool +} + +func (o *ReconcileClusterRoleBindingOptions) Run() (*ReconcileClusterRoleBindingResult, error) { + return o.run(0) +} + +func (o *ReconcileClusterRoleBindingOptions) run(attempts int) (*ReconcileClusterRoleBindingResult, error) { + // This keeps us from retrying forever if a rolebinding keeps appearing and disappearing as we reconcile. + // Conflict errors on update are handled at a higher level. + if attempts > 3 { + return nil, fmt.Errorf("exceeded maximum attempts") + } + + var result *ReconcileClusterRoleBindingResult + + existingBinding, err := o.Client.Get(o.RoleBinding.Name, metav1.GetOptions{}) + switch { + case errors.IsNotFound(err): + result = &ReconcileClusterRoleBindingResult{ + RoleBinding: o.RoleBinding, + MissingSubjects: o.RoleBinding.Subjects, + Operation: ReconcileCreate, + } + + case err != nil: + return nil, err + + default: + result, err = computeReconciledRoleBinding(existingBinding, o.RoleBinding, o.RemoveExtraSubjects) + if err != nil { + return nil, err + } + } + + // If reconcile-protected, short-circuit + if result.Protected { + return result, nil + } + // If we're in dry-run mode, short-circuit + if !o.Confirm { + return result, nil + } + + switch result.Operation { + case ReconcileRecreate: + // Try deleting + err := o.Client.Delete( + existingBinding.Name, + &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &existingBinding.UID}}, + ) + switch { + case err == nil, errors.IsNotFound(err): + // object no longer exists, as desired + case errors.IsConflict(err): + // delete failed because our UID precondition conflicted + // this could mean another object exists with a different UID, re-run + return o.run(attempts + 1) + default: + // return other errors + return nil, err + } + // continue to create + fallthrough + case ReconcileCreate: + created, err := o.Client.Create(result.RoleBinding) + // If created since we started this reconcile, re-run + if errors.IsAlreadyExists(err) { + return o.run(attempts + 1) + } + if err != nil { + return nil, err + } + result.RoleBinding = created + + case ReconcileUpdate: + updated, err := o.Client.Update(result.RoleBinding) + // If deleted since we started this reconcile, re-run + if errors.IsNotFound(err) { + return o.run(attempts + 1) + } + if err != nil { + return nil, err + } + result.RoleBinding = updated + + case ReconcileNone: + // no-op + + default: + return nil, fmt.Errorf("invalid operation: %v", result.Operation) + } + + return result, nil +} + +// computeReconciledRoleBinding returns the rolebinding that must be created and/or updated to make the +// existing rolebinding's subjects, roleref, labels, and annotations match the expected rolebinding +func computeReconciledRoleBinding(existing, expected *rbac.ClusterRoleBinding, removeExtraSubjects bool) (*ReconcileClusterRoleBindingResult, error) { + result := &ReconcileClusterRoleBindingResult{Operation: ReconcileNone} + + result.Protected = (existing.Annotations[rbac.AutoUpdateAnnotationKey] == "false") + + // Reset the binding completely if the roleRef is different + if expected.RoleRef != existing.RoleRef { + result.RoleBinding = expected + result.Operation = ReconcileRecreate + return result, nil + } + + // Start with a copy of the existing object + changedObj, err := api.Scheme.DeepCopy(existing) + if err != nil { + return nil, err + } + result.RoleBinding = changedObj.(*rbac.ClusterRoleBinding) + + // Merge expected annotations and labels + result.RoleBinding.Annotations = merge(expected.Annotations, result.RoleBinding.Annotations) + if !reflect.DeepEqual(result.RoleBinding.Annotations, existing.Annotations) { + result.Operation = ReconcileUpdate + } + result.RoleBinding.Labels = merge(expected.Labels, result.RoleBinding.Labels) + if !reflect.DeepEqual(result.RoleBinding.Labels, existing.Labels) { + result.Operation = ReconcileUpdate + } + + // Compute extra and missing subjects + result.MissingSubjects, result.ExtraSubjects = diffSubjectLists(expected.Subjects, existing.Subjects) + + switch { + case !removeExtraSubjects && len(result.MissingSubjects) > 0: + // add missing subjects in the union case + result.RoleBinding.Subjects = append(result.RoleBinding.Subjects, result.MissingSubjects...) + result.Operation = ReconcileUpdate + + case removeExtraSubjects && (len(result.MissingSubjects) > 0 || len(result.ExtraSubjects) > 0): + // stomp to expected subjects in the non-union case + result.RoleBinding.Subjects = expected.Subjects + result.Operation = ReconcileUpdate + } + + return result, nil +} + +func contains(list []rbac.Subject, item rbac.Subject) bool { + for _, listItem := range list { + if listItem == item { + return true + } + } + return false +} + +// diffSubjectLists returns lists containing the items unique to each provided list: +// list1Only = list1 - list2 +// list2Only = list2 - list1 +// if both returned lists are empty, the provided lists are equal +func diffSubjectLists(list1 []rbac.Subject, list2 []rbac.Subject) (list1Only []rbac.Subject, list2Only []rbac.Subject) { + for _, list1Item := range list1 { + if !contains(list2, list1Item) { + if !contains(list1Only, list1Item) { + list1Only = append(list1Only, list1Item) + } + } + } + for _, list2Item := range list2 { + if !contains(list1, list2Item) { + if !contains(list2Only, list2Item) { + list2Only = append(list2Only, list2Item) + } + } + } + return +} diff --git a/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings_test.go b/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings_test.go new file mode 100644 index 00000000000..e0839f94b6a --- /dev/null +++ b/pkg/registry/rbac/reconciliation/reconcile_clusterrolebindings_test.go @@ -0,0 +1,179 @@ +/* +Copyright 2017 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 reconciliation + +import ( + "testing" + + api "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/rbac" +) + +func binding(roleRef rbac.RoleRef, subjects []rbac.Subject) *rbac.ClusterRoleBinding { + return &rbac.ClusterRoleBinding{RoleRef: roleRef, Subjects: subjects} +} + +func ref(name string) rbac.RoleRef { + return rbac.RoleRef{Name: name} +} + +func subject(name string) rbac.Subject { + return rbac.Subject{Name: name} +} + +func subjects(names ...string) []rbac.Subject { + r := []rbac.Subject{} + for _, name := range names { + r = append(r, subject(name)) + } + return r +} + +func TestDiffObjectReferenceLists(t *testing.T) { + tests := map[string]struct { + A []rbac.Subject + B []rbac.Subject + ExpectedOnlyA []rbac.Subject + ExpectedOnlyB []rbac.Subject + }{ + "empty": {}, + + "matching, order-independent": { + A: subjects("foo", "bar"), + B: subjects("bar", "foo"), + }, + + "partial match": { + A: subjects("foo", "bar"), + B: subjects("foo", "baz"), + ExpectedOnlyA: subjects("bar"), + ExpectedOnlyB: subjects("baz"), + }, + + "missing": { + A: subjects("foo"), + B: subjects("bar"), + ExpectedOnlyA: subjects("foo"), + ExpectedOnlyB: subjects("bar"), + }, + + "remove duplicates": { + A: subjects("foo", "foo"), + B: subjects("bar", "bar"), + ExpectedOnlyA: subjects("foo"), + ExpectedOnlyB: subjects("bar"), + }, + } + + for k, tc := range tests { + onlyA, onlyB := diffSubjectLists(tc.A, tc.B) + if !api.Semantic.DeepEqual(onlyA, tc.ExpectedOnlyA) { + t.Errorf("%s: Expected %#v, got %#v", k, tc.ExpectedOnlyA, onlyA) + } + if !api.Semantic.DeepEqual(onlyB, tc.ExpectedOnlyB) { + t.Errorf("%s: Expected %#v, got %#v", k, tc.ExpectedOnlyB, onlyB) + } + } +} + +func TestComputeUpdate(t *testing.T) { + tests := map[string]struct { + ExpectedBinding *rbac.ClusterRoleBinding + ActualBinding *rbac.ClusterRoleBinding + RemoveExtraSubjects bool + + ExpectedUpdatedBinding *rbac.ClusterRoleBinding + ExpectedUpdateNeeded bool + }{ + "match without union": { + ExpectedBinding: binding(ref("role"), subjects("a")), + ActualBinding: binding(ref("role"), subjects("a")), + RemoveExtraSubjects: true, + + ExpectedUpdatedBinding: nil, + ExpectedUpdateNeeded: false, + }, + "match with union": { + ExpectedBinding: binding(ref("role"), subjects("a")), + ActualBinding: binding(ref("role"), subjects("a")), + RemoveExtraSubjects: false, + + ExpectedUpdatedBinding: nil, + ExpectedUpdateNeeded: false, + }, + + "different roleref with identical subjects": { + ExpectedBinding: binding(ref("role"), subjects("a")), + ActualBinding: binding(ref("differentRole"), subjects("a")), + RemoveExtraSubjects: false, + + ExpectedUpdatedBinding: binding(ref("role"), subjects("a")), + ExpectedUpdateNeeded: true, + }, + + "extra subjects without union": { + ExpectedBinding: binding(ref("role"), subjects("a")), + ActualBinding: binding(ref("role"), subjects("a", "b")), + RemoveExtraSubjects: true, + + ExpectedUpdatedBinding: binding(ref("role"), subjects("a")), + ExpectedUpdateNeeded: true, + }, + "extra subjects with union": { + ExpectedBinding: binding(ref("role"), subjects("a")), + ActualBinding: binding(ref("role"), subjects("a", "b")), + RemoveExtraSubjects: false, + + ExpectedUpdatedBinding: nil, + ExpectedUpdateNeeded: false, + }, + + "missing subjects without union": { + ExpectedBinding: binding(ref("role"), subjects("a", "c")), + ActualBinding: binding(ref("role"), subjects("a", "b")), + RemoveExtraSubjects: true, + + ExpectedUpdatedBinding: binding(ref("role"), subjects("a", "c")), + ExpectedUpdateNeeded: true, + }, + "missing subjects with union": { + ExpectedBinding: binding(ref("role"), subjects("a", "c")), + ActualBinding: binding(ref("role"), subjects("a", "b")), + RemoveExtraSubjects: false, + + ExpectedUpdatedBinding: binding(ref("role"), subjects("a", "b", "c")), + ExpectedUpdateNeeded: true, + }, + } + + for k, tc := range tests { + result, err := computeReconciledRoleBinding(tc.ActualBinding, tc.ExpectedBinding, tc.RemoveExtraSubjects) + if err != nil { + t.Errorf("%s: %v", k, err) + continue + } + updateNeeded := result.Operation != ReconcileNone + updatedBinding := result.RoleBinding + if updateNeeded != tc.ExpectedUpdateNeeded { + t.Errorf("%s: Expected\n\t%v\ngot\n\t%v (%v)", k, tc.ExpectedUpdateNeeded, updateNeeded, result.Operation) + continue + } + if updateNeeded && !api.Semantic.DeepEqual(updatedBinding, tc.ExpectedUpdatedBinding) { + t.Errorf("%s: Expected\n\t%v %v\ngot\n\t%v %v", k, tc.ExpectedUpdatedBinding.RoleRef, tc.ExpectedUpdatedBinding.Subjects, updatedBinding.RoleRef, updatedBinding.Subjects) + } + } +} diff --git a/pkg/registry/rbac/rest/BUILD b/pkg/registry/rbac/rest/BUILD index baf005b84da..0a79675bb4c 100644 --- a/pkg/registry/rbac/rest/BUILD +++ b/pkg/registry/rbac/rest/BUILD @@ -17,12 +17,14 @@ go_library( "//pkg/apis/rbac/v1alpha1:go_default_library", "//pkg/apis/rbac/v1beta1:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion:go_default_library", + "//pkg/client/retry:go_default_library", "//pkg/registry/rbac/clusterrole:go_default_library", "//pkg/registry/rbac/clusterrole/policybased:go_default_library", "//pkg/registry/rbac/clusterrole/storage:go_default_library", "//pkg/registry/rbac/clusterrolebinding:go_default_library", "//pkg/registry/rbac/clusterrolebinding/policybased:go_default_library", "//pkg/registry/rbac/clusterrolebinding/storage:go_default_library", + "//pkg/registry/rbac/reconciliation:go_default_library", "//pkg/registry/rbac/role:go_default_library", "//pkg/registry/rbac/role/policybased:go_default_library", "//pkg/registry/rbac/role/storage:go_default_library", @@ -32,7 +34,6 @@ go_library( "//pkg/registry/rbac/validation:go_default_library", "//plugin/pkg/auth/authorizer/rbac/bootstrappolicy:go_default_library", "//vendor:github.com/golang/glog", - "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/runtime/schema", "//vendor:k8s.io/apimachinery/pkg/util/runtime", "//vendor:k8s.io/apimachinery/pkg/util/wait", diff --git a/pkg/registry/rbac/rest/storage_rbac.go b/pkg/registry/rbac/rest/storage_rbac.go index b9fa6ce9ba0..e1fc4420187 100644 --- a/pkg/registry/rbac/rest/storage_rbac.go +++ b/pkg/registry/rbac/rest/storage_rbac.go @@ -23,7 +23,6 @@ import ( "github.com/golang/glog" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -36,12 +35,14 @@ import ( rbacapiv1alpha1 "k8s.io/kubernetes/pkg/apis/rbac/v1alpha1" rbacapiv1beta1 "k8s.io/kubernetes/pkg/apis/rbac/v1beta1" rbacclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" + "k8s.io/kubernetes/pkg/client/retry" "k8s.io/kubernetes/pkg/registry/rbac/clusterrole" clusterrolepolicybased "k8s.io/kubernetes/pkg/registry/rbac/clusterrole/policybased" clusterrolestore "k8s.io/kubernetes/pkg/registry/rbac/clusterrole/storage" "k8s.io/kubernetes/pkg/registry/rbac/clusterrolebinding" clusterrolebindingpolicybased "k8s.io/kubernetes/pkg/registry/rbac/clusterrolebinding/policybased" clusterrolebindingstore "k8s.io/kubernetes/pkg/registry/rbac/clusterrolebinding/storage" + "k8s.io/kubernetes/pkg/registry/rbac/reconciliation" "k8s.io/kubernetes/pkg/registry/rbac/role" rolepolicybased "k8s.io/kubernetes/pkg/registry/rbac/role/policybased" rolestore "k8s.io/kubernetes/pkg/registry/rbac/role/storage" @@ -133,37 +134,62 @@ func PostStartHook(hookContext genericapiserver.PostStartHookContext) error { return false, nil } - existingClusterRoles, err := clientset.ClusterRoles().List(metav1.ListOptions{}) - if err != nil { - utilruntime.HandleError(fmt.Errorf("unable to initialize clusterroles: %v", err)) - return false, nil - } - // only initialized on empty etcd - if len(existingClusterRoles.Items) == 0 { - for _, clusterRole := range append(bootstrappolicy.ClusterRoles(), bootstrappolicy.ControllerRoles()...) { - if _, err := clientset.ClusterRoles().Create(&clusterRole); err != nil { - // don't fail on failures, try to create as many as you can - utilruntime.HandleError(fmt.Errorf("unable to initialize clusterroles: %v", err)) - continue + // ensure bootstrap roles are created or reconciled + for _, clusterRole := range append(bootstrappolicy.ClusterRoles(), bootstrappolicy.ControllerRoles()...) { + opts := reconciliation.ReconcileClusterRoleOptions{ + Role: &clusterRole, + Client: clientset.ClusterRoles(), + Confirm: true, + } + err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + result, err := opts.Run() + if err != nil { + return err } - glog.Infof("Created clusterrole.%s/%s", rbac.GroupName, clusterRole.Name) + switch { + case result.Protected && result.Operation != reconciliation.ReconcileNone: + glog.Warningf("skipped reconcile-protected clusterrole.%s/%s with missing permissions: %v", rbac.GroupName, clusterRole.Name, result.MissingRules) + case result.Operation == reconciliation.ReconcileUpdate: + glog.Infof("updated clusterrole.%s/%s with additional permissions: %v", rbac.GroupName, clusterRole.Name, result.MissingRules) + case result.Operation == reconciliation.ReconcileCreate: + glog.Infof("created clusterrole.%s/%s", rbac.GroupName, clusterRole.Name) + } + return nil + }) + if err != nil { + // don't fail on failures, try to create as many as you can + utilruntime.HandleError(fmt.Errorf("unable to reconcile clusterrole.%s/%s: %v", rbac.GroupName, clusterRole.Name, err)) } } - existingClusterRoleBindings, err := clientset.ClusterRoleBindings().List(metav1.ListOptions{}) - if err != nil { - utilruntime.HandleError(fmt.Errorf("unable to initialize clusterrolebindings: %v", err)) - return false, nil - } - // only initialized on empty etcd - if len(existingClusterRoleBindings.Items) == 0 { - for _, clusterRoleBinding := range append(bootstrappolicy.ClusterRoleBindings(), bootstrappolicy.ControllerRoleBindings()...) { - if _, err := clientset.ClusterRoleBindings().Create(&clusterRoleBinding); err != nil { - // don't fail on failures, try to create as many as you can - utilruntime.HandleError(fmt.Errorf("unable to initialize clusterrolebindings: %v", err)) - continue + // ensure bootstrap rolebindings are created or reconciled + for _, clusterRoleBinding := range append(bootstrappolicy.ClusterRoleBindings(), bootstrappolicy.ControllerRoleBindings()...) { + + opts := reconciliation.ReconcileClusterRoleBindingOptions{ + RoleBinding: &clusterRoleBinding, + Client: clientset.ClusterRoleBindings(), + Confirm: true, + } + err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + result, err := opts.Run() + if err != nil { + return err } - glog.Infof("Created clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name) + switch { + case result.Protected && result.Operation != reconciliation.ReconcileNone: + glog.Warningf("skipped reconcile-protected clusterrolebinding.%s/%s with missing subjects: %v", rbac.GroupName, clusterRoleBinding.Name, result.MissingSubjects) + case result.Operation == reconciliation.ReconcileUpdate: + glog.Infof("updated clusterrolebinding.%s/%s with additional subjects: %v", rbac.GroupName, clusterRoleBinding.Name, result.MissingSubjects) + case result.Operation == reconciliation.ReconcileCreate: + glog.Infof("created clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name) + case result.Operation == reconciliation.ReconcileRecreate: + glog.Infof("recreated clusterrolebinding.%s/%s", rbac.GroupName, clusterRoleBinding.Name) + } + return nil + }) + if err != nil { + // don't fail on failures, try to create as many as you can + utilruntime.HandleError(fmt.Errorf("unable to reconcile clusterrolebinding.%s/%s: %v", rbac.GroupName, clusterRoleBinding.Name, err)) } } diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go index 03ecd7ac34d..d34f15f0a58 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go @@ -26,7 +26,8 @@ var ( ReadWrite = []string{"get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"} Read = []string{"get", "list", "watch"} - Label = map[string]string{"kubernetes.io/bootstrapping": "rbac-defaults"} + Label = map[string]string{"kubernetes.io/bootstrapping": "rbac-defaults"} + Annotation = map[string]string{rbac.AutoUpdateAnnotationKey: "true"} ) const ( @@ -51,6 +52,13 @@ func addClusterRoleLabel(roles []rbac.ClusterRole) { for k, v := range Label { roles[i].ObjectMeta.Labels[k] = v } + + if roles[i].ObjectMeta.Annotations == nil { + roles[i].ObjectMeta.Annotations = make(map[string]string) + } + for k, v := range Annotation { + roles[i].ObjectMeta.Annotations[k] = v + } } return } @@ -63,6 +71,13 @@ func addClusterRoleBindingLabel(rolebindings []rbac.ClusterRoleBinding) { for k, v := range Label { rolebindings[i].ObjectMeta.Labels[k] = v } + + if rolebindings[i].ObjectMeta.Annotations == nil { + rolebindings[i].ObjectMeta.Annotations = make(map[string]string) + } + for k, v := range Annotation { + rolebindings[i].ObjectMeta.Annotations[k] = v + } } return } diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-role-bindings.yaml b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-role-bindings.yaml index 02937c51ca2..edf89a47d91 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-role-bindings.yaml +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-role-bindings.yaml @@ -3,6 +3,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -18,6 +20,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -36,6 +40,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -54,6 +60,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -69,6 +77,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -84,6 +94,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml index 55dc4794983..884db5bd648 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/cluster-roles.yaml @@ -3,6 +3,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -159,6 +161,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -177,6 +181,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -313,6 +319,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -333,6 +341,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -347,6 +357,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -365,6 +377,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -384,6 +398,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -401,6 +417,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -506,6 +524,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -618,6 +638,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -635,6 +657,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -663,6 +687,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -693,6 +719,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -742,6 +770,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-role-bindings.yaml b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-role-bindings.yaml index 6f7e5dd7465..eaa946c2cd2 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-role-bindings.yaml +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-role-bindings.yaml @@ -3,6 +3,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -18,6 +20,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -33,6 +37,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -48,6 +54,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -63,6 +71,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -78,6 +88,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -93,6 +105,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -108,6 +122,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -123,6 +139,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -138,6 +156,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -153,6 +173,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -168,6 +190,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -183,6 +207,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -198,6 +224,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -213,6 +241,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -228,6 +258,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -243,6 +275,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -258,6 +292,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -273,6 +309,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -288,6 +326,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -303,6 +343,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -318,6 +360,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-roles.yaml b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-roles.yaml index ed720ca59b2..4f486535d0f 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-roles.yaml +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/testdata/controller-roles.yaml @@ -3,6 +3,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -49,6 +51,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -80,6 +84,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -129,6 +135,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -181,6 +189,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -233,6 +243,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -295,6 +307,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -336,6 +350,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -363,6 +379,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -439,6 +457,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -479,6 +499,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -512,6 +534,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -556,6 +580,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -643,6 +669,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -665,6 +693,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -706,6 +736,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -747,6 +779,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -776,6 +810,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -805,6 +841,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -827,6 +865,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -864,6 +904,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults @@ -917,6 +959,8 @@ items: - apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" creationTimestamp: null labels: kubernetes.io/bootstrapping: rbac-defaults