| /* |
| 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" |
| |
| rbacv1 "k8s.io/api/rbac/v1" |
| "k8s.io/apimachinery/pkg/api/errors" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/types" |
| ) |
| |
| type RoleBindingModifier interface { |
| Get(namespace, name string) (RoleBinding, error) |
| Delete(namespace, name string, uid types.UID) error |
| Create(RoleBinding) (RoleBinding, error) |
| Update(RoleBinding) (RoleBinding, error) |
| } |
| |
| type RoleBinding interface { |
| GetObject() runtime.Object |
| GetNamespace() string |
| GetName() string |
| GetUID() types.UID |
| GetLabels() map[string]string |
| SetLabels(map[string]string) |
| GetAnnotations() map[string]string |
| SetAnnotations(map[string]string) |
| GetRoleRef() rbacv1.RoleRef |
| GetSubjects() []rbacv1.Subject |
| SetSubjects([]rbacv1.Subject) |
| DeepCopyRoleBinding() RoleBinding |
| } |
| |
| // ReconcileRoleBindingOptions holds options for running a role binding reconciliation |
| type ReconcileRoleBindingOptions struct { |
| // RoleBinding is the expected rolebinding that will be reconciled |
| RoleBinding RoleBinding |
| // 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 RoleBindingModifier |
| } |
| |
| // 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 RoleBinding |
| |
| // MissingSubjects contains expected subjects that were missing from the currently persisted rolebinding |
| MissingSubjects []rbacv1.Subject |
| // ExtraSubjects contains extra subjects the currently persisted rolebinding had |
| ExtraSubjects []rbacv1.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 *ReconcileRoleBindingOptions) Run() (*ReconcileClusterRoleBindingResult, error) { |
| return o.run(0) |
| } |
| |
| func (o *ReconcileRoleBindingOptions) 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.GetNamespace(), o.RoleBinding.GetName()) |
| switch { |
| case errors.IsNotFound(err): |
| result = &ReconcileClusterRoleBindingResult{ |
| RoleBinding: o.RoleBinding, |
| MissingSubjects: o.RoleBinding.GetSubjects(), |
| 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.GetNamespace(), existingBinding.GetName(), existingBinding.GetUID()) |
| 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 RoleBinding, removeExtraSubjects bool) (*ReconcileClusterRoleBindingResult, error) { |
| result := &ReconcileClusterRoleBindingResult{Operation: ReconcileNone} |
| |
| result.Protected = (existing.GetAnnotations()[rbacv1.AutoUpdateAnnotationKey] == "false") |
| |
| // Reset the binding completely if the roleRef is different |
| if expected.GetRoleRef() != existing.GetRoleRef() { |
| result.RoleBinding = expected |
| result.Operation = ReconcileRecreate |
| return result, nil |
| } |
| |
| // Start with a copy of the existing object |
| result.RoleBinding = existing.DeepCopyRoleBinding() |
| |
| // Merge expected annotations and labels |
| result.RoleBinding.SetAnnotations(merge(expected.GetAnnotations(), result.RoleBinding.GetAnnotations())) |
| if !reflect.DeepEqual(result.RoleBinding.GetAnnotations(), existing.GetAnnotations()) { |
| result.Operation = ReconcileUpdate |
| } |
| result.RoleBinding.SetLabels(merge(expected.GetLabels(), result.RoleBinding.GetLabels())) |
| if !reflect.DeepEqual(result.RoleBinding.GetLabels(), existing.GetLabels()) { |
| result.Operation = ReconcileUpdate |
| } |
| |
| // Compute extra and missing subjects |
| result.MissingSubjects, result.ExtraSubjects = diffSubjectLists(expected.GetSubjects(), existing.GetSubjects()) |
| |
| switch { |
| case !removeExtraSubjects && len(result.MissingSubjects) > 0: |
| // add missing subjects in the union case |
| result.RoleBinding.SetSubjects(append(result.RoleBinding.GetSubjects(), 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.SetSubjects(expected.GetSubjects()) |
| result.Operation = ReconcileUpdate |
| } |
| |
| return result, nil |
| } |
| |
| func contains(list []rbacv1.Subject, item rbacv1.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 []rbacv1.Subject, list2 []rbacv1.Subject) (list1Only []rbacv1.Subject, list2Only []rbacv1.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 |
| } |