mirror of https://github.com/k3s-io/k3s
249 lines
8.2 KiB
Go
249 lines
8.2 KiB
Go
/*
|
|
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
|
|
}
|