// checkAndDecrement checks if the provided PodDisruptionBudget allows any disruption. func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb policy.PodDisruptionBudget) (ok bool, err error) { if pdb.Status.ObservedGeneration < pdb.Generation { return false, nil } if pdb.Status.PodDisruptionsAllowed < 0 { return false, errors.NewForbidden(policy.Resource("poddisruptionbudget"), pdb.Name, fmt.Errorf("pdb disruptions allowed is negative")) } if len(pdb.Status.DisruptedPods) > MaxDisruptedPodSize { return false, errors.NewForbidden(policy.Resource("poddisruptionbudget"), pdb.Name, fmt.Errorf("DisrputedPods map too big - too many evictions not confirmed by PDB controller")) } if pdb.Status.PodDisruptionsAllowed == 0 { return false, nil } pdb.Status.PodDisruptionsAllowed-- if pdb.Status.DisruptedPods == nil { pdb.Status.DisruptedPods = make(map[string]metav1.Time) } // Eviction handler needs to inform the PDB controller that it is about to delete a pod // so it should not consider it as available in calculations when updating PodDisruptions allowed. // If the pod is not deleted within a reasonable time limit PDB controller will assume that it won't // be deleted at all and remove it from DisruptedPod map. pdb.Status.DisruptedPods[podName] = metav1.Time{Time: time.Now()} if _, err := r.podDisruptionBudgetClient.PodDisruptionBudgets(namespace).UpdateStatus(&pdb); err != nil { return false, err } return true, nil }
func (s *Storage) Update(ctx genericapirequest.Context, name string, obj rest.UpdatedObjectInfo) (runtime.Object, bool, error) { if rbacregistry.EscalationAllowed(ctx) { return s.StandardStorage.Update(ctx, name, obj) } nonEscalatingInfo := rest.WrapUpdatedObjectInfo(obj, func(ctx genericapirequest.Context, obj runtime.Object, oldObj runtime.Object) (runtime.Object, error) { clusterRoleBinding := obj.(*rbac.ClusterRoleBinding) // if we're explicitly authorized to bind this clusterrole, return if rbacregistry.BindingAuthorized(ctx, clusterRoleBinding.RoleRef, clusterRoleBinding.Namespace, s.authorizer) { return obj, nil } // Otherwise, see if we already have all the permissions contained in the referenced clusterrole rules, err := s.ruleResolver.GetRoleReferenceRules(clusterRoleBinding.RoleRef, clusterRoleBinding.Namespace) if err != nil { return nil, err } if err := rbacregistryvalidation.ConfirmNoEscalation(ctx, s.ruleResolver, rules); err != nil { return nil, errors.NewForbidden(groupResource, clusterRoleBinding.Name, err) } return obj, nil }) return s.StandardStorage.Update(ctx, name, nonEscalatingInfo) }
// Admit will deny any pod that defines AntiAffinity topology key other than metav1.LabelHostname i.e. "kubernetes.io/hostname" // in requiredDuringSchedulingRequiredDuringExecution and requiredDuringSchedulingIgnoredDuringExecution. func (p *plugin) Admit(attributes admission.Attributes) (err error) { // Ignore all calls to subresources or resources other than pods. if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") { return nil } pod, ok := attributes.GetObject().(*api.Pod) if !ok { return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") } affinity := pod.Spec.Affinity if affinity != nil && affinity.PodAntiAffinity != nil { var podAntiAffinityTerms []api.PodAffinityTerm if len(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) != 0 { podAntiAffinityTerms = affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution } // TODO: Uncomment this block when implement RequiredDuringSchedulingRequiredDuringExecution. //if len(affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution) != 0 { // podAntiAffinityTerms = append(podAntiAffinityTerms, affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution...) //} for _, v := range podAntiAffinityTerms { if v.TopologyKey != metav1.LabelHostname { return apierrors.NewForbidden(attributes.GetResource().GroupResource(), pod.Name, fmt.Errorf("affinity.PodAntiAffinity.RequiredDuringScheduling has TopologyKey %v but only key %v is allowed", v.TopologyKey, metav1.LabelHostname)) } } } return nil }
// NewForbidden is a utility function to return a well-formatted admission control error response func NewForbidden(a Attributes, internalError error) error { // do not double wrap an error of same type if apierrors.IsForbidden(internalError) { return internalError } name, resource, err := extractResourceName(a) if err != nil { return apierrors.NewInternalError(utilerrors.NewAggregate([]error{internalError, err})) } return apierrors.NewForbidden(resource, name, internalError) }
func (s *Storage) Create(ctx genericapirequest.Context, obj runtime.Object) (runtime.Object, error) { if rbacregistry.EscalationAllowed(ctx) { return s.StandardStorage.Create(ctx, obj) } clusterRole := obj.(*rbac.ClusterRole) rules := clusterRole.Rules if err := rbacregistryvalidation.ConfirmNoEscalation(ctx, s.ruleResolver, rules); err != nil { return nil, errors.NewForbidden(groupResource, clusterRole.Name, err) } return s.StandardStorage.Create(ctx, obj) }
// Admit will deny any pod that defines SELinuxOptions or RunAsUser. func (p *plugin) Admit(a admission.Attributes) (err error) { if a.GetSubresource() != "" || a.GetResource().GroupResource() != api.Resource("pods") { return nil } pod, ok := a.GetObject().(*api.Pod) if !ok { return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") } if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SupplementalGroups != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.SupplementalGroups is forbidden")) } if pod.Spec.SecurityContext != nil { if pod.Spec.SecurityContext.SELinuxOptions != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.SELinuxOptions is forbidden")) } if pod.Spec.SecurityContext.RunAsUser != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.RunAsUser is forbidden")) } } if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.FSGroup != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.FSGroup is forbidden")) } for _, v := range pod.Spec.InitContainers { if v.SecurityContext != nil { if v.SecurityContext.SELinuxOptions != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden")) } if v.SecurityContext.RunAsUser != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden")) } } } for _, v := range pod.Spec.Containers { if v.SecurityContext != nil { if v.SecurityContext.SELinuxOptions != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden")) } if v.SecurityContext.RunAsUser != nil { return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden")) } } } return nil }
func (s *Storage) Update(ctx genericapirequest.Context, name string, obj rest.UpdatedObjectInfo) (runtime.Object, bool, error) { if rbacregistry.EscalationAllowed(ctx) { return s.StandardStorage.Update(ctx, name, obj) } nonEscalatingInfo := rest.WrapUpdatedObjectInfo(obj, func(ctx genericapirequest.Context, obj runtime.Object, oldObj runtime.Object) (runtime.Object, error) { clusterRole := obj.(*rbac.ClusterRole) rules := clusterRole.Rules if err := rbacregistryvalidation.ConfirmNoEscalation(ctx, s.ruleResolver, rules); err != nil { return nil, errors.NewForbidden(groupResource, clusterRole.Name, err) } return obj, nil }) return s.StandardStorage.Update(ctx, name, nonEscalatingInfo) }
func (s *Storage) Create(ctx genericapirequest.Context, obj runtime.Object) (runtime.Object, error) { if rbacregistry.EscalationAllowed(ctx) { return s.StandardStorage.Create(ctx, obj) } clusterRoleBinding := obj.(*rbac.ClusterRoleBinding) if rbacregistry.BindingAuthorized(ctx, clusterRoleBinding.RoleRef, clusterRoleBinding.Namespace, s.authorizer) { return s.StandardStorage.Create(ctx, obj) } rules, err := s.ruleResolver.GetRoleReferenceRules(clusterRoleBinding.RoleRef, clusterRoleBinding.Namespace) if err != nil { return nil, err } if err := rbacregistryvalidation.ConfirmNoEscalation(ctx, s.ruleResolver, rules); err != nil { return nil, errors.NewForbidden(groupResource, clusterRoleBinding.Name, err) } return s.StandardStorage.Create(ctx, obj) }
// Admit enforces that pod and its namespace node label selectors matches at least a node in the cluster. func (p *podNodeSelector) Admit(a admission.Attributes) error { resource := a.GetResource().GroupResource() if resource != api.Resource("pods") { return nil } if a.GetSubresource() != "" { // only run the checks below on pods proper and not subresources return nil } obj := a.GetObject() pod, ok := obj.(*api.Pod) if !ok { glog.Errorf("expected pod but got %s", a.GetKind().Kind) return nil } if !p.WaitForReady() { return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) } name := pod.Name nsName := a.GetNamespace() var namespace *api.Namespace namespaceObj, exists, err := p.namespaceInformer.GetStore().Get(&api.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: nsName, Namespace: "", }, }) if err != nil { return errors.NewInternalError(err) } if exists { namespace = namespaceObj.(*api.Namespace) } else { namespace, err = p.defaultGetNamespace(nsName) if err != nil { if errors.IsNotFound(err) { return err } return errors.NewInternalError(err) } } namespaceNodeSelector, err := p.getNodeSelectorMap(namespace) if err != nil { return err } if labels.Conflicts(namespaceNodeSelector, labels.Set(pod.Spec.NodeSelector)) { return errors.NewForbidden(resource, name, fmt.Errorf("pod node label selector conflicts with its namespace node label selector")) } whitelist, err := labels.ConvertSelectorToLabelsMap(p.clusterNodeSelectors[namespace.Name]) if err != nil { return err } // Merge pod node selector = namespace node selector + current pod node selector podNodeSelectorLabels := labels.Merge(namespaceNodeSelector, pod.Spec.NodeSelector) // whitelist verification if !labels.AreLabelsInWhiteList(podNodeSelectorLabels, whitelist) { return errors.NewForbidden(resource, name, fmt.Errorf("pod node label selector labels conflict with its namespace whitelist")) } // Updated pod node selector = namespace node selector + current pod node selector pod.Spec.NodeSelector = map[string]string(podNodeSelectorLabels) return nil }
func (l *lifecycle) Admit(a admission.Attributes) error { // prevent deletion of immortal namespaces if a.GetOperation() == admission.Delete && a.GetKind().GroupKind() == api.Kind("Namespace") && l.immortalNamespaces.Has(a.GetName()) { return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("this namespace may not be deleted")) } // if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do // if we're here, then the API server has found a route, which means that if we have a non-empty namespace // its a namespaced resource. if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") { // if a namespace is deleted, we want to prevent all further creates into it // while it is undergoing termination. to reduce incidences where the cache // is slow to update, we add the namespace into a force live lookup list to ensure // we are not looking at stale state. if a.GetOperation() == admission.Delete { l.forceLiveLookupCache.Add(a.GetName(), true, forceLiveLookupTTL) } return nil } // we need to wait for our caches to warm if !l.WaitForReady() { return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) } var ( namespaceObj interface{} exists bool err error ) key := makeNamespaceKey(a.GetNamespace()) namespaceObj, exists, err = l.namespaceInformer.GetStore().Get(key) if err != nil { return errors.NewInternalError(err) } if !exists && a.GetOperation() == admission.Create { // give the cache time to observe the namespace before rejecting a create. // this helps when creating a namespace and immediately creating objects within it. time.Sleep(missingNamespaceWait) namespaceObj, exists, err = l.namespaceInformer.GetStore().Get(key) if err != nil { return errors.NewInternalError(err) } if exists { glog.V(4).Infof("found %s in cache after waiting", a.GetNamespace()) } } // forceLiveLookup if true will skip looking at local cache state and instead always make a live call to server. forceLiveLookup := false if _, ok := l.forceLiveLookupCache.Get(a.GetNamespace()); ok { // we think the namespace was marked for deletion, but our current local cache says otherwise, we will force a live lookup. forceLiveLookup = exists && namespaceObj.(*api.Namespace).Status.Phase == api.NamespaceActive } // refuse to operate on non-existent namespaces if !exists || forceLiveLookup { // as a last resort, make a call directly to storage namespaceObj, err = l.client.Core().Namespaces().Get(a.GetNamespace(), metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return err } return errors.NewInternalError(err) } glog.V(4).Infof("found %s via storage lookup", a.GetNamespace()) } // ensure that we're not trying to create objects in terminating namespaces if a.GetOperation() == admission.Create { namespace := namespaceObj.(*api.Namespace) if namespace.Status.Phase != api.NamespaceTerminating { return nil } // TODO: This should probably not be a 403 return admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated.", a.GetNamespace())) } return nil }
func TestTokenCreation(t *testing.T) { testcases := map[string]struct { ClientObjects []runtime.Object IsAsync bool MaxRetries int Reactors []reaction ExistingServiceAccount *v1.ServiceAccount ExistingSecrets []*v1.Secret AddedServiceAccount *v1.ServiceAccount UpdatedServiceAccount *v1.ServiceAccount DeletedServiceAccount *v1.ServiceAccount AddedSecret *v1.Secret UpdatedSecret *v1.Secret DeletedSecret *v1.Secret ExpectedActions []core.Action }{ "new serviceaccount with no secrets": { ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(emptySecretReferences()))), }, }, "new serviceaccount with no secrets encountering create error": { ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, MaxRetries: 10, IsAsync: true, Reactors: []reaction{{ verb: "create", resource: "secrets", reactor: func(t *testing.T) core.ReactionFunc { i := 0 return func(core.Action) (bool, runtime.Object, error) { i++ if i < 3 { return true, nil, apierrors.NewForbidden(api.Resource("secrets"), "foo", errors.New("No can do")) } return false, nil, nil } }, }}, AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ // Attempt 1 core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), // Attempt 2 core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, namedCreatedTokenSecret("default-token-x50vb")), // Attempt 3 core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, namedCreatedTokenSecret("default-token-scq98")), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addNamedTokenSecretReference(emptySecretReferences(), "default-token-scq98"))), }, }, "new serviceaccount with no secrets encountering unending create error": { ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, MaxRetries: 2, IsAsync: true, Reactors: []reaction{{ verb: "create", resource: "secrets", reactor: func(t *testing.T) core.ReactionFunc { return func(core.Action) (bool, runtime.Object, error) { return true, nil, apierrors.NewForbidden(api.Resource("secrets"), "foo", errors.New("No can do")) } }, }}, AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ // Attempt core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), // Retry 1 core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, namedCreatedTokenSecret("default-token-x50vb")), // Retry 2 core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, namedCreatedTokenSecret("default-token-scq98")), }, }, "new serviceaccount with missing secrets": { ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())}, AddedServiceAccount: serviceAccount(missingSecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(missingSecretReferences()))), }, }, "new serviceaccount with non-token secrets": { ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()}, AddedServiceAccount: serviceAccount(regularSecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(regularSecretReferences()))), }, }, "new serviceaccount with token secrets": { ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()}, ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()}, AddedServiceAccount: serviceAccount(tokenSecretReferences()), ExpectedActions: []core.Action{}, }, "new serviceaccount with no secrets with resource conflict": { ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences()), createdTokenSecret()}, AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), }, }, "updated serviceaccount with no secrets": { ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, UpdatedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(emptySecretReferences()))), }, }, "updated serviceaccount with missing secrets": { ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())}, UpdatedServiceAccount: serviceAccount(missingSecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(missingSecretReferences()))), }, }, "updated serviceaccount with non-token secrets": { ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()}, UpdatedServiceAccount: serviceAccount(regularSecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, createdTokenSecret()), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(addTokenSecretReference(regularSecretReferences()))), }, }, "updated serviceaccount with token secrets": { ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()}, UpdatedServiceAccount: serviceAccount(tokenSecretReferences()), ExpectedActions: []core.Action{}, }, "updated serviceaccount with no secrets with resource conflict": { ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences())}, UpdatedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), }, }, "deleted serviceaccount with no secrets": { DeletedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{}, }, "deleted serviceaccount with missing secrets": { DeletedServiceAccount: serviceAccount(missingSecretReferences()), ExpectedActions: []core.Action{}, }, "deleted serviceaccount with non-token secrets": { ClientObjects: []runtime.Object{opaqueSecret()}, DeletedServiceAccount: serviceAccount(regularSecretReferences()), ExpectedActions: []core.Action{}, }, "deleted serviceaccount with token secrets": { ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()}, DeletedServiceAccount: serviceAccount(tokenSecretReferences()), ExpectedActions: []core.Action{ core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), }, }, "added secret without serviceaccount": { ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, AddedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), }, }, "added secret with serviceaccount": { ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{}, }, "added token secret without token data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecretWithoutTokenData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "added token secret without ca data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutCAData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecretWithoutCAData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "added token secret with mismatched ca data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "added token secret without namespace data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecretWithoutNamespaceData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "added token secret with custom namespace data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), AddedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")), ExpectedActions: []core.Action{ // no update is performed... the custom namespace is preserved }, }, "updated secret without serviceaccount": { ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, UpdatedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), }, }, "updated secret with serviceaccount": { ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{}, }, "updated token secret without token data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "updated token secret without ca data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutCAData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecretWithoutCAData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "updated token secret with mismatched ca data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "updated token secret without namespace data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecretWithoutNamespaceData(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, "token-secret-1"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, v1.NamespaceDefault, serviceAccountTokenSecret()), }, }, "updated token secret with custom namespace data": { ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), UpdatedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")), ExpectedActions: []core.Action{ // no update is performed... the custom namespace is preserved }, }, "deleted secret without serviceaccount": { DeletedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{}, }, "deleted secret with serviceaccount with reference": { ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences())}, ExistingServiceAccount: serviceAccount(tokenSecretReferences()), DeletedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, serviceAccount(emptySecretReferences())), }, }, "deleted secret with serviceaccount without reference": { ExistingServiceAccount: serviceAccount(emptySecretReferences()), DeletedSecret: serviceAccountTokenSecret(), ExpectedActions: []core.Action{ core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, v1.NamespaceDefault, "default"), }, }, } for k, tc := range testcases { glog.Infof(k) // Re-seed to reset name generation utilrand.Seed(1) generator := &testGenerator{Token: "ABC"} client := fake.NewSimpleClientset(tc.ClientObjects...) for _, reactor := range tc.Reactors { client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactor(t)) } controller := NewTokensController(client, TokensControllerOptions{TokenGenerator: generator, RootCA: []byte("CA Data"), MaxRetries: tc.MaxRetries}) if tc.ExistingServiceAccount != nil { controller.serviceAccounts.Add(tc.ExistingServiceAccount) } for _, s := range tc.ExistingSecrets { controller.secrets.Add(s) } if tc.AddedServiceAccount != nil { controller.serviceAccounts.Add(tc.AddedServiceAccount) controller.queueServiceAccountSync(tc.AddedServiceAccount) } if tc.UpdatedServiceAccount != nil { controller.serviceAccounts.Add(tc.UpdatedServiceAccount) controller.queueServiceAccountUpdateSync(nil, tc.UpdatedServiceAccount) } if tc.DeletedServiceAccount != nil { controller.serviceAccounts.Delete(tc.DeletedServiceAccount) controller.queueServiceAccountSync(tc.DeletedServiceAccount) } if tc.AddedSecret != nil { controller.secrets.Add(tc.AddedSecret) controller.queueSecretSync(tc.AddedSecret) } if tc.UpdatedSecret != nil { controller.secrets.Add(tc.UpdatedSecret) controller.queueSecretUpdateSync(nil, tc.UpdatedSecret) } if tc.DeletedSecret != nil { controller.secrets.Delete(tc.DeletedSecret) controller.queueSecretSync(tc.DeletedSecret) } // This is the longest we'll wait for async tests timeout := time.Now().Add(30 * time.Second) waitedForAdditionalActions := false for { if controller.syncServiceAccountQueue.Len() > 0 { controller.syncServiceAccount() } if controller.syncSecretQueue.Len() > 0 { controller.syncSecret() } // The queues still have things to work on if controller.syncServiceAccountQueue.Len() > 0 || controller.syncSecretQueue.Len() > 0 { continue } // If we expect this test to work asynchronously... if tc.IsAsync { // if we're still missing expected actions within our test timeout if len(client.Actions()) < len(tc.ExpectedActions) && time.Now().Before(timeout) { // wait for the expected actions (without hotlooping) time.Sleep(time.Millisecond) continue } // if we exactly match our expected actions, wait a bit to make sure no other additional actions show up if len(client.Actions()) == len(tc.ExpectedActions) && !waitedForAdditionalActions { time.Sleep(time.Second) waitedForAdditionalActions = true continue } } break } if controller.syncServiceAccountQueue.Len() > 0 { t.Errorf("%s: unexpected items in service account queue: %d", k, controller.syncServiceAccountQueue.Len()) } if controller.syncSecretQueue.Len() > 0 { t.Errorf("%s: unexpected items in secret queue: %d", k, controller.syncSecretQueue.Len()) } actions := client.Actions() for i, action := range actions { if len(tc.ExpectedActions) < i+1 { t.Errorf("%s: %d unexpected actions: %+v", k, len(actions)-len(tc.ExpectedActions), actions[i:]) break } expectedAction := tc.ExpectedActions[i] if !reflect.DeepEqual(expectedAction, action) { t.Errorf("%s:\nExpected:\n%s\ngot:\n%s", k, spew.Sdump(expectedAction), spew.Sdump(action)) continue } } if len(tc.ExpectedActions) > len(actions) { t.Errorf("%s: %d additional expected actions", k, len(tc.ExpectedActions)-len(actions)) for _, a := range tc.ExpectedActions[len(actions):] { t.Logf(" %+v", a) } } } }