diff --git a/deploy/crds/kabanero.io_kabaneros_crd.yaml b/deploy/crds/kabanero.io_kabaneros_crd.yaml index 6048bdf8..0de910a3 100644 --- a/deploy/crds/kabanero.io_kabaneros_crd.yaml +++ b/deploy/crds/kabanero.io_kabaneros_crd.yaml @@ -980,6 +980,22 @@ spec: version: type: string type: object + targetNamespaces: + description: Target namespace status + properties: + message: + type: string + namespaces: + description: These are the target namespaces that are currently + being used. The spec.targetNamespaces will replace these when + the operator has finished applying the role bindings to those + namespaces. + items: + type: string + type: array + ready: + type: string + type: object tekton: description: Tekton instance readiness status. properties: diff --git a/pkg/apis/kabanero/v1alpha2/kabanero_types.go b/pkg/apis/kabanero/v1alpha2/kabanero_types.go index a7155875..323bf01e 100644 --- a/pkg/apis/kabanero/v1alpha2/kabanero_types.go +++ b/pkg/apis/kabanero/v1alpha2/kabanero_types.go @@ -281,6 +281,19 @@ type KabaneroStatus struct { Sso SsoStatus `json:"sso,omitempty"` Gitops GitopsStatus `json:"gitops,omitempty"` + + // Target namespace status + TargetNamespaces TargetNamespaceStatus `json:"targetNamespaces,omitempty"` +} + +type TargetNamespaceStatus struct { + // These are the target namespaces that are currently being used. The + // spec.targetNamespaces will replace these when the operator has finished + // applying the role bindings to those namespaces. + // +listType=set + Namespaces []string `json:"namespaces,omitempty"` + Ready string `json:"ready,omitempty"` + Message string `json:"message,omitempty"` } // PipelineStatus defines the observed state of the assets located within a single pipeline .tar.gz. diff --git a/pkg/apis/kabanero/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/kabanero/v1alpha2/zz_generated.deepcopy.go index 5959947a..f9776db6 100644 --- a/pkg/apis/kabanero/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/kabanero/v1alpha2/zz_generated.deepcopy.go @@ -695,6 +695,7 @@ func (in *KabaneroStatus) DeepCopyInto(out *KabaneroStatus) { out.AdmissionControllerWebhook = in.AdmissionControllerWebhook out.Sso = in.Sso in.Gitops.DeepCopyInto(&out.Gitops) + in.TargetNamespaces.DeepCopyInto(&out.TargetNamespaces) return } @@ -1071,6 +1072,27 @@ func (in *StackVersionStatus) DeepCopy() *StackVersionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetNamespaceStatus) DeepCopyInto(out *TargetNamespaceStatus) { + *out = *in + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetNamespaceStatus. +func (in *TargetNamespaceStatus) DeepCopy() *TargetNamespaceStatus { + if in == nil { + return nil + } + out := new(TargetNamespaceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TektonStatus) DeepCopyInto(out *TektonStatus) { *out = *in diff --git a/pkg/controller/kabaneroplatform/gitops_test.go b/pkg/controller/kabaneroplatform/gitops_test.go index 8965ee6f..61712305 100644 --- a/pkg/controller/kabaneroplatform/gitops_test.go +++ b/pkg/controller/kabaneroplatform/gitops_test.go @@ -28,7 +28,11 @@ type testLogger struct{} func (t testLogger) Info(msg string, keysAndValues ...interface{}) { fmt.Printf("Info: %v \n", msg) } func (t testLogger) Enabled() bool { return true } func (t testLogger) Error(err error, msg string, keysAndValues ...interface{}) { - fmt.Printf("Error: %v: %v\n", msg, err.Error()) + if err != nil { + fmt.Printf("Error: %v: %v\n", msg, err.Error()) + } else { + fmt.Printf("Error: %v\n", msg) + } } func (t testLogger) V(level int) logr.InfoLogger { return t } func (t testLogger) WithValues(keysAndValues ...interface{}) logr.Logger { return t } diff --git a/pkg/controller/kabaneroplatform/kabaneroplatform_controller.go b/pkg/controller/kabaneroplatform/kabaneroplatform_controller.go index 34961147..f581c789 100644 --- a/pkg/controller/kabaneroplatform/kabaneroplatform_controller.go +++ b/pkg/controller/kabaneroplatform/kabaneroplatform_controller.go @@ -13,6 +13,7 @@ import ( kabanerov1alpha1 "github.com/kabanero-io/kabanero-operator/pkg/apis/kabanero/v1alpha1" kabanerov1alpha2 "github.com/kabanero-io/kabanero-operator/pkg/apis/kabanero/v1alpha2" "github.com/kabanero-io/kabanero-operator/pkg/controller/utils/timer" + "github.com/operator-framework/operator-sdk/pkg/k8sutil" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -54,24 +55,30 @@ var reconcileFuncs = []reconcileFuncType{ {name: "events", function: reconcileEvents}, {name: "sso", function: reconcileSso}, {name: "gitops", function: reconcileGitopsPipelines}, + {name: "target namespaces", function: reconcileTargetNamespaces}, } // Add creates a new Kabanero Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(mgr manager.Manager) error { - return add(mgr, newReconciler(mgr)) -} + // It is very unlikely that this would fail, since the main also checks for it. + watchNamespace, err := k8sutil.GetWatchNamespace() + if err != nil { + return err + } -// newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileKabanero{ + // Lets be sure a single namespace is specified. + numberOfWatchNamespaces := len(strings.Split(watchNamespace, ",")) + if numberOfWatchNamespaces != 1 { + return fmt.Errorf("%v watch namespaces were specified, but only a single watch namespace is supported: %v", numberOfWatchNamespaces, watchNamespace) + } + + r := &ReconcileKabanero{ client: mgr.GetClient(), scheme: mgr.GetScheme(), - requeueDelayMap: make(map[string]RequeueData)} -} - -// add adds a new Controller to mgr with r as the reconcile.Reconciler -func add(mgr manager.Manager, r reconcile.Reconciler) error { + requeueDelayMap: make(map[string]RequeueData), + watchNamespace: watchNamespace} + // Create a new controller c, err := controller.New("kabaneroplatform-controller", mgr, controller.Options{Reconciler: r}) if err != nil { @@ -104,6 +111,16 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } + // Watch Namespace instances. We only care about create and delete events, not update events. + // When we see that a namespace has been created/deleted, we need to process any Kabanero objects that + // reference that namespace. + err = c.Watch(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestsFromMapFunc{ + ToRequests: handler.ToRequestsFunc(r.targetNamespaceMapFunc)}, predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { return false }}) + if err != nil { + return err + } + /* Useful if RoleBindingList is changed to use Structured instead of Unstructured // Index Rolebindings by name if err := mgr.GetFieldIndexer().IndexField(&rbacv1.RoleBinding{}, "metadata.name", func(rawObj runtime.Object) []string { @@ -117,22 +134,17 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return nil } -func getOperatorImage(c client.Client) (string, error) { +func (r *ReconcileKabanero) getOperatorImage() (string, error) { // First, read the POD_NAME env variable. This is set in the deployment spec in the CSV. podName := os.Getenv("POD_NAME") if len(podName) == 0 { return "", fmt.Errorf("The POD_NAME environment variable is not set, or is empty") } - namespace := os.Getenv("WATCH_NAMESPACE") - if len(namespace) == 0 { - return "", fmt.Errorf("The WATCH_NAMESPACE environment variable is not set, or is empty") - } - // Second, get the Pod instance with that name pod := &corev1.Pod{} - kubePodName := types.NamespacedName{Name: podName, Namespace: namespace} - err := c.Get(context.TODO(), kubePodName, pod) + kubePodName := types.NamespacedName{Name: podName, Namespace: r.watchNamespace} + err := r.client.Get(context.TODO(), kubePodName, pod) if err != nil { return "", fmt.Errorf("Pod %v could not be retrieved: %v", podName, err.Error()) } @@ -177,6 +189,7 @@ type ReconcileKabanero struct { client client.Client scheme *runtime.Scheme requeueDelayMap map[string]RequeueData + watchNamespace string } // RequeueData stores information that enables reconcile operations to be retried. @@ -185,6 +198,33 @@ type RequeueData struct { futureTime time.Time } +// When we see that a namespace has changed, we want to reconcile any Kabanero instances that +// reference that namespace in its targetNamespaces list. +func (r *ReconcileKabanero) targetNamespaceMapFunc(a handler.MapObject) []reconcile.Request { + log.Info(fmt.Sprintf("Processing for change in namespace %v", a.Meta.GetName())) + + // List Kabanero instances + kabaneros := &kabanerov1alpha2.KabaneroList{} + err := r.client.List(context.TODO(), kabaneros, client.InNamespace(r.watchNamespace)) + if err != nil { + log.Error(err, fmt.Sprintf("Could not process namespace event for \"%v\"", a.Meta.GetName())) + return nil + } + + // For each Kabanero instance, if spec.targetNamespaces includes a.meta.name then add a reconcile request. + requests := []reconcile.Request{} + for _, kabanero := range kabaneros.Items { + for _, namespace := range kabanero.Spec.TargetNamespaces { + if namespace == a.Meta.GetName() { + requests = append(requests, reconcile.Request{types.NamespacedName{Name: kabanero.Name, Namespace: kabanero.Namespace}}) + break + } + } + } + + return requests +} + // Determine if requeue is needed or not. // If requeue is required set RequeueAfter to 60 seconds the first time. // After the first time increase RequeueAfter by 60 seconds up to a max of 15 minutes. @@ -288,7 +328,7 @@ func (r *ReconcileKabanero) Reconcile(request reconcile.Request) (reconcile.Resu // in the add() method because the client is not started yet (that would have been ideal). operatorContainerImageOp.Do(func() { var err error - operatorContainerImage, err = getOperatorImage(r.client) + operatorContainerImage, err = r.getOperatorImage() if err != nil { log.Error(err, "Could not read the kabanero-operator container image from the pod") } @@ -380,13 +420,6 @@ func (r *ReconcileKabanero) Reconcile(request reconcile.Request) (reconcile.Resu } } - // Reconcile the targetNamespaces - err = reconcileTargetNamespaces(ctx, instance, r.client, reqLogger) - if err != nil { - reqLogger.Error(err, "Error reconciling targetNamespaces") - return reconcile.Result{}, err - } - // Deploy feature collection resources. err = reconcileFeaturedStacks(ctx, instance, r.client, reqLogger) if err != nil { @@ -515,6 +548,12 @@ func cleanup(ctx context.Context, k *kabanerov1alpha2.Kabanero, client client.Cl if err != nil { return err } + + // Remove the cross-namespace objects that target namespaces use. + err = cleanupTargetNamespaces(ctx, k, client) + if err != nil { + return err + } return nil } @@ -553,6 +592,7 @@ func processStatus(ctx context.Context, request reconcile.Request, k *kabanerov1 isAdmissionControllerWebhookReady, _ := getAdmissionControllerWebhookStatus(k, c, reqLogger) isSsoReady, _ := getSsoStatus(k, c, reqLogger) isGitopsReady, _ := getGitopsStatus(k) + isTargetNamespacesReady, _ := getTargetNamespacesStatus(k) // Set the overall status. isKabaneroReady := isCollectionControllerReady && @@ -567,7 +607,8 @@ func processStatus(ctx context.Context, request reconcile.Request, k *kabanerov1 isEventsReady && isAdmissionControllerWebhookReady && isSsoReady && - isGitopsReady + isGitopsReady && + isTargetNamespacesReady if isKabaneroReady { k.Status.KabaneroInstance.Message = "" diff --git a/pkg/controller/kabaneroplatform/targetnamespaces.go b/pkg/controller/kabaneroplatform/targetnamespaces.go index 03175f43..171122ee 100644 --- a/pkg/controller/kabaneroplatform/targetnamespaces.go +++ b/pkg/controller/kabaneroplatform/targetnamespaces.go @@ -1,6 +1,9 @@ package kabaneroplatform import ( "context" + "errors" + "fmt" + "strings" kabanerov1alpha2 "github.com/kabanero-io/kabanero-operator/pkg/apis/kabanero/v1alpha2" @@ -9,122 +12,211 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" ) -func reconcileTargetNamespaces(ctx context.Context, k *kabanerov1alpha2.Kabanero, cl client.Client, reqLogger logr.Logger) error { +type targetNamespaceRoleBindingTemplate struct { + name string + saName string + saNamespace string + clusterRoleName string +} - // Rolebinding Template - ownerIsController := true - rolebindingResource := rbacv1.RoleBinding{ +func (info targetNamespaceRoleBindingTemplate) generate(targetNamespace string) rbacv1.RoleBinding { + return rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: "kabanero-pipeline-deploy-rolebinding", - OwnerReferences: []metav1.OwnerReference{ - metav1.OwnerReference{ - APIVersion: k.TypeMeta.APIVersion, - Kind: k.TypeMeta.Kind, - Name: k.ObjectMeta.Name, - UID: k.ObjectMeta.UID, - Controller: &ownerIsController, - }, - }, + Name: info.name, + Namespace: targetNamespace, }, Subjects: []rbacv1.Subject{ rbacv1.Subject{ Kind: "ServiceAccount", - Name: "kabanero-pipeline", - Namespace: "kabanero", + Name: info.saName, + Namespace: info.saNamespace, }, }, RoleRef: rbacv1.RoleRef{ Kind: "ClusterRole", - Name: "kabanero-pipeline-deploy-role", + Name: info.clusterRoleName, APIGroup: "rbac.authorization.k8s.io", }, } +} - - // List of Namespaces we want to bind - targetnamespaceList := k.Spec.TargetNamespaces +// We're going to target the current namespace, and the list of target +// namespaces from the Kabanero CR instance. +func getTargetNamespaces(targetNamespaces []string, defaultNamespace string) []string { + targetnamespaceList := targetNamespaces // If targetNamespaces is empty, default to binding to kabanero if len(targetnamespaceList) == 0 { - targetnamespaceList = append(targetnamespaceList, "kabanero") + targetnamespaceList = append(targetnamespaceList, defaultNamespace) } - /* Get all Rolebindings named kabanero-pipeline-deploy-rolebinding - Structured method only lists RoleBindings in kabanero namespace. Maybe due to client scoping? - - rolebindingList := &rbacv1.RoleBindingList{} - err := cl.List(ctx, rolebindingList, client.MatchingFields{"metadata.name": "kabanero-pipeline-deploy-rolebinding"}) - if err != nil { - return err + return targetnamespaceList +} + +// Create the binding templates +func createBindingTemplates(saNamespace string) []targetNamespaceRoleBindingTemplate{ + return []targetNamespaceRoleBindingTemplate { + { + name: "kabanero-pipeline-deploy-rolebinding", + saName: "kabanero-pipeline", + saNamespace: saNamespace, + clusterRoleName: "kabanero-pipeline-deploy-role", + }, + // TODO: Second role binding for CLI service } - */ +} - // Get all Rolebindings named kabanero-pipeline-deploy-rolebinding - rolebindingList := &unstructured.UnstructuredList{} - rolebindingList.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "rbac.authorization.k8s.io", - Kind: "RoleBindingList", - Version: "v1", - }) - err := cl.List(ctx, rolebindingList, client.MatchingFields{"metadata.name": "kabanero-pipeline-deploy-rolebinding"}) - if err != nil { - return err +func reconcileTargetNamespaces(ctx context.Context, k *kabanerov1alpha2.Kabanero, cl client.Client, reqLogger logr.Logger) error { + + // Owner reference for same-namespace bindings + ownerIsController := true + ownerReference := metav1.OwnerReference{ + APIVersion: k.TypeMeta.APIVersion, + Kind: k.TypeMeta.Kind, + Name: k.ObjectMeta.Name, + UID: k.ObjectMeta.UID, + Controller: &ownerIsController, } - // For each Rolebinding - for _, rolebinding := range rolebindingList.Items { - matchFound := false - // If the Rolebinding namespace is in the targetNamespace list - for i, targetnamespace := range targetnamespaceList { - if rolebinding.GetNamespace() == targetnamespace { - - // Fill in the namespace for the template - desiredRolebinding := rolebindingResource - desiredRolebinding.ObjectMeta.Namespace = targetnamespace - - // Check if the existing Rolebinding matches the desired template, and skip? - // Update may handle this already - - // Apply the rolebinding - cl.Update(ctx, &desiredRolebinding) - - // Remove the namespace from the list of Namespaces remaining to bind - targetnamespaceList[i] = targetnamespaceList[len(targetnamespaceList)-1] // Copy last element to index i. - targetnamespaceList[len(targetnamespaceList)-1] = "" // Erase last element (write zero value). - targetnamespaceList = targetnamespaceList[:len(targetnamespaceList)-1] // Truncate slice. - - matchFound = true - break + // Be sure each requested namespace exists. This will catch namespaces added to the list, as well as + // namespaces that were deleted but not removed from the targetNamespaces list. + specTargetNamespaces := sets.NewString(getTargetNamespaces(k.Spec.TargetNamespaces, k.GetNamespace())...) + var errorNamespaces []string + for namespace, _ := range specTargetNamespaces { + exists, err := namespaceExists(ctx, namespace, cl) + if err != nil { + reqLogger.Error(err, fmt.Sprintf("Could not check status of namespace %v", namespace)) + errorNamespaces = append(errorNamespaces, namespace) + } + if exists == false { + reqLogger.Error(nil, fmt.Sprintf("Target namespace %v does not exist", namespace)) + errorNamespaces = append(errorNamespaces, namespace) + } + } + + for _, namespace := range errorNamespaces { + delete(specTargetNamespaces, namespace) + } + + // TODO: did I do this right? need to process the namespaces, then look at errorNamespaces and + // generate an error message for namespaces that did not exist. Once we have a watch set + // up, that should take care of partially active lists, and the delete case. + + // Compute the new, deleted, and common namespace names + statusTargetNamespaces := sets.NewString(getTargetNamespaces(k.Status.TargetNamespaces.Namespaces, k.GetNamespace())...) + oldNamespaces := statusTargetNamespaces.Difference(specTargetNamespaces) + newNamespaces := specTargetNamespaces.Difference(statusTargetNamespaces) + unchangedNamespaces := specTargetNamespaces.Intersection(statusTargetNamespaces) + + // Create the templates + bindingTemplates := createBindingTemplates(k.GetNamespace()) + + // For removed namespaces, delete the role bindings + for namespace, _ := range oldNamespaces { + for _, bindingTemplate := range bindingTemplates { + template := bindingTemplate.generate(namespace) + reqLogger.Info(fmt.Sprintf("Deleting RoleBinding %v for removed target namespace %v", template.GetName(), template.GetNamespace())) + cl.Delete(ctx, &template) + } + } + + // For new namespaces, create the role bindings + for namespace, _ := range newNamespaces { + for _, bindingTemplate := range bindingTemplates { + template := bindingTemplate.generate(namespace) + if k.GetNamespace() == namespace { + template.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerReference} } + reqLogger.Info(fmt.Sprintf("Creating RoleBinding %v for added target namespace %v", template.GetName(), template.GetNamespace())) + cl.Create(ctx, &template) } + } - // If the Rolebinding does not match a targetNamespace, delete - if matchFound == false { - cl.Delete(ctx, &rolebinding) + // For unchanged namespaces, validate the role bindings + for namespace, _ := range unchangedNamespaces { + for _, bindingTemplate := range bindingTemplates { + template := bindingTemplate.generate(namespace) + if k.GetNamespace() == namespace { + template.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerReference} + } + reqLogger.Info(fmt.Sprintf("Updating RoleBinding %v for unchanged target namespace %v", template.GetName(), template.GetNamespace())) + cl.Update(ctx, &template) } } - - // For remaining namespaces in the list, Create the Rolebinding - for _, targetnamespace := range targetnamespaceList { - // Check if the namespace exists - namespace := &unstructured.Unstructured{} - namespace.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "", - Kind: "Namespace", - Version: "v1", - }) - err = cl.Get(ctx, client.ObjectKey{Namespace: targetnamespace, Name: targetnamespace,}, namespace) - if err == nil { - // Fill in the namespace for the template and Create - desiredRolebinding := rolebindingResource - desiredRolebinding.ObjectMeta.Namespace = targetnamespace - cl.Create(ctx, &desiredRolebinding) + + // Update the Status to reflect the new target namespaces. + k.Status.TargetNamespaces.Namespaces = nil + for _, namespace := range k.Spec.TargetNamespaces { + isErrorNamespace := false + for _, errorNamespace := range errorNamespaces { + if errorNamespace == namespace { + isErrorNamespace = true + break + } + } + if isErrorNamespace == false { + k.Status.TargetNamespaces.Namespaces = append(k.Status.TargetNamespaces.Namespaces, namespace) } } - + + if len(errorNamespaces) == 0 { + k.Status.TargetNamespaces.Ready = "True" + k.Status.TargetNamespaces.Message = "" + } else { + k.Status.TargetNamespaces.Ready = "False" + k.Status.TargetNamespaces.Message = fmt.Sprintf("The following namespaces could not be processed: %v", strings.Join(errorNamespaces, ",")) + return errors.New(k.Status.TargetNamespaces.Message) + } + + return nil +} + +// Checks if a namespace exists. If an unknown error occurs, return that too. +func namespaceExists(ctx context.Context, inNamespace string, cl client.Client) (bool, error) { + namespace := &unstructured.Unstructured{} + namespace.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Namespace", + Version: "v1", + }) + err := cl.Get(ctx, client.ObjectKey{Namespace: inNamespace, Name: inNamespace,}, namespace) + if err == nil { + return true, nil + } + + if kerrors.IsNotFound(err) { + return false, nil + } + + return false, err +} + +// Returns the readiness status of the target namespaces. Presently the status +// is determined as the namespaces are activated. We are just reporting that +// status here. +func getTargetNamespacesStatus(k *kabanerov1alpha2.Kabanero) (bool, error) { + return k.Status.TargetNamespaces.Ready == "True", nil +} + +// Clean up the cross-namespace bindings that we created (deleting the +// Kabanero CR instance won't delete these because cross-namespace owner +// references are not allowed by Kubernetes). +func cleanupTargetNamespaces(ctx context.Context, k *kabanerov1alpha2.Kabanero, cl client.Client) error { + // Create the templates + bindingTemplates := createBindingTemplates(k.GetNamespace()) + + for _, namespace := range getTargetNamespaces(k.Status.TargetNamespaces.Namespaces, k.GetNamespace()) { + for _, bindingTemplate := range bindingTemplates { + template := bindingTemplate.generate(namespace) + cl.Delete(ctx, &template) + } + } + return nil } diff --git a/pkg/controller/kabaneroplatform/targetnamespaces_test.go b/pkg/controller/kabaneroplatform/targetnamespaces_test.go new file mode 100644 index 00000000..7caa736e --- /dev/null +++ b/pkg/controller/kabaneroplatform/targetnamespaces_test.go @@ -0,0 +1,342 @@ +package kabaneroplatform + +import ( + "context" + "errors" + "fmt" + + kabanerov1alpha2 "github.com/kabanero-io/kabanero-operator/pkg/apis/kabanero/v1alpha2" + apierrors "k8s.io/apimachinery/pkg/api/errors" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "testing" +) + +func init() { + logf.SetLogger(testLogger{}) +} + +var nslog = logf.Log.WithName("targetnamespaces_test") + +// Unit test Kube client +type targetnamespaceTestClient struct { + // Role bindings that the client knows about. + objs map[client.ObjectKey]bool + + // Namespaces that the client knows about + namespaces map[string]bool +} + +func (c targetnamespaceTestClient) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error { + fmt.Printf("Received Get() for %v\n", key.Name) + u, ok := obj.(*unstructured.Unstructured) + if !ok { + fmt.Printf("Received invalid target object for get: %v\n", obj) + return errors.New("Get only supports setting into Unstructured") + } + _, ok = c.namespaces[key.Name] + if !ok { + return apierrors.NewNotFound(schema.GroupResource{}, key.Name) + } + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + return nil +} +func (c targetnamespaceTestClient) List(ctx context.Context, list runtime.Object, opts ...client.ListOption) error { + return nil +} +func (c targetnamespaceTestClient) Create(ctx context.Context, obj runtime.Object, opts ...client.CreateOption) error { + binding, ok := obj.(*rbacv1.RoleBinding) + if !ok { + fmt.Printf("Received invalid create: %v\n", obj) + return errors.New("Create only supports RoleBinding") + } + + fmt.Printf("Received Create() for %v\n", binding.GetName()) + key := client.ObjectKey{Name: binding.GetName(), Namespace: binding.GetNamespace()} + _, ok = c.objs[key] + if ok { + fmt.Printf("Receive create object already exists: %v/%v\n", binding.GetNamespace(), binding.GetName()) + return apierrors.NewAlreadyExists(schema.GroupResource{}, binding.GetName()) + } + + c.objs[key] = true + return nil +} +func (c targetnamespaceTestClient) Delete(ctx context.Context, obj runtime.Object, opts ...client.DeleteOption) error { + binding, ok := obj.(*rbacv1.RoleBinding) + if !ok { + fmt.Printf("Received invalid delete: %v\n", obj) + return errors.New("Delete only supports RoleBinding") + } + + fmt.Printf("Received Delete() for %v\n", binding.GetName()) + key := client.ObjectKey{Name: binding.GetName(), Namespace: binding.GetNamespace()} + _, ok = c.objs[key] + if !ok { + fmt.Printf("Received delete for an object that does not exist: %v\n", obj) + return apierrors.NewNotFound(schema.GroupResource{}, binding.GetName()) + } + delete(c.objs, key) + return nil +} +func (c targetnamespaceTestClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error { + return errors.New("DeleteAllOf is not supported") +} +func (c targetnamespaceTestClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOption) error { + binding, ok := obj.(*rbacv1.RoleBinding) + if !ok { + fmt.Printf("Received invalid update: %v\n", obj) + return errors.New("Update only supports RoleBinding") + } + + fmt.Printf("Received Update() for %v\n", binding.GetName()) + key := client.ObjectKey{Name: binding.GetName(), Namespace: binding.GetNamespace()} + _, ok = c.objs[key] + if !ok { + fmt.Printf("Received update for object that does not exist: %v\n", obj) + return apierrors.NewNotFound(schema.GroupResource{}, binding.GetName()) + } + return nil +} +func (c targetnamespaceTestClient) Status() client.StatusWriter { return c } + +func (c targetnamespaceTestClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOption) error { + return errors.New("Patch is not supported") +} + +// Apply the role bindings to an existing namespace +func TestReconcileTargetNamespaces(t *testing.T) { + targetNamespace := "fred" + k := kabanerov1alpha2.Kabanero{ + ObjectMeta: metav1.ObjectMeta{Name: "kabanero", Namespace: "kabanero"}, + Spec: kabanerov1alpha2.KabaneroSpec{ + TargetNamespaces: []string{targetNamespace}, + }, + } + + existingNamespaces := make(map[string]bool) + existingNamespaces[targetNamespace] = true + client := targetnamespaceTestClient{map[client.ObjectKey]bool{}, existingNamespaces} + + err := reconcileTargetNamespaces(context.TODO(), &k, client, nslog) + + if err != nil { + t.Fatal("Returned error: " + err.Error()) + } + + // Make sure the kabanero status was updated with the target namespace + if len(k.Status.TargetNamespaces.Namespaces) != 1 { + t.Fatal(fmt.Sprintf("Kabanero status should have 1 target namespace, but has %v: %v", len(k.Status.TargetNamespaces.Namespaces), k.Status.TargetNamespaces.Namespaces)) + } + + if k.Status.TargetNamespaces.Namespaces[0] != targetNamespace { + t.Fatal(fmt.Sprintf("Kabanero status target namespace should be %v, but is %v", targetNamespace, k.Status.TargetNamespaces.Namespaces[0])) + } + + if k.Status.TargetNamespaces.Ready != "True" { + t.Fatal(fmt.Sprintf("Kabanero target namespace status is not True: %v", k.Status.TargetNamespaces.Ready)) + } + + if len(k.Status.TargetNamespaces.Message) != 0 { + t.Fatal(fmt.Sprintf("Kabanero target namespace status contains an error message: %v", k.Status.TargetNamespaces.Message)) + } + + // Make sure the RoleBinding got added in the correct namespace. + if len(client.objs) != 1 { + t.Fatal(fmt.Sprintf("Should have created one RoleBinding, but created %v: %#v", len(client.objs), client.objs)) + } + + for key, _ := range client.objs { + if key.Namespace != targetNamespace { + t.Fatal(fmt.Sprintf("Should have created RoleBinding in %v namespace, but created in %v namespace", targetNamespace, key.Namespace)) + } + } +} + +// Apply the role bindings to a namespace that does not exist. +func TestReconcileTargetNamespacesNamespaceNotExist(t *testing.T) { + targetNamespace := "fred" + activeNamespace := "kabanero" + k := kabanerov1alpha2.Kabanero{ + ObjectMeta: metav1.ObjectMeta{Name: "kabanero", Namespace: "kabanero"}, + Spec: kabanerov1alpha2.KabaneroSpec{ + TargetNamespaces: []string{targetNamespace}, + }, + Status: kabanerov1alpha2.KabaneroStatus { + TargetNamespaces: kabanerov1alpha2.TargetNamespaceStatus { + Namespaces: []string{activeNamespace}, + Ready: "True", + }, + }, + } + + // Set up pre-existing objects + existingNamespaces := make(map[string]bool) + existingNamespaces[activeNamespace] = true + existingRoleBinding := client.ObjectKey{Name: "kabanero-pipeline-deploy-rolebinding", Namespace: activeNamespace} + existingRoleBindings := make(map[client.ObjectKey]bool) + existingRoleBindings[existingRoleBinding] = true + client := targetnamespaceTestClient{existingRoleBindings, existingNamespaces} + + err := reconcileTargetNamespaces(context.TODO(), &k, client, nslog) + + if err == nil { + t.Fatal("Did not return an error, but should have because namespace does not exist") + } + + // Make sure the kabanero status was not updated with the target namespace, + // since it did not exist. + if len(k.Status.TargetNamespaces.Namespaces) != 0 { + t.Fatal(fmt.Sprintf("Kabanero status should have 0 target namespace, but has %v: %v", len(k.Status.TargetNamespaces.Namespaces), k.Status.TargetNamespaces.Namespaces)) + } + + if k.Status.TargetNamespaces.Ready != "False" { + t.Fatal(fmt.Sprintf("Kabanero target namespace status is not False: %v", k.Status.TargetNamespaces.Ready)) + } + + if len(k.Status.TargetNamespaces.Message) == 0 { + t.Fatal("Kabanero target namespace status contains no error message") + } + + // Make sure the RoleBinding map got cleared. + if len(client.objs) != 0 { + t.Fatal(fmt.Sprintf("Should have created 0 RoleBindings, but created %v: %#v", len(client.objs), client.objs)) + } + + // OK, now create the namespace and make sure things resolve as per normal. + existingNamespaces[targetNamespace] = true + + err = reconcileTargetNamespaces(context.TODO(), &k, client, nslog) + + if err != nil { + t.Fatal("Returned error: " + err.Error()) + } + + // Make sure the kabanero status was updated with the target namespace + if len(k.Status.TargetNamespaces.Namespaces) != 1 { + t.Fatal(fmt.Sprintf("Kabanero status should have 1 target namespace, but has %v: %v", len(k.Status.TargetNamespaces.Namespaces), k.Status.TargetNamespaces.Namespaces)) + } + + if k.Status.TargetNamespaces.Namespaces[0] != targetNamespace { + t.Fatal(fmt.Sprintf("After NS create, Kabanero status target namespace should be %v, but is %v", targetNamespace, k.Status.TargetNamespaces.Namespaces[0])) + } + + if k.Status.TargetNamespaces.Ready != "True" { + t.Fatal(fmt.Sprintf("After NS create, Kabanero target namespace status is not True: %v", k.Status.TargetNamespaces.Ready)) + } + + if len(k.Status.TargetNamespaces.Message) != 0 { + t.Fatal(fmt.Sprintf("After NS create, Kabanero target namespace status contains an error message: %v", k.Status.TargetNamespaces.Message)) + } + + // Make sure the RoleBinding got added in the correct namespace. + if len(client.objs) != 1 { + t.Fatal(fmt.Sprintf("After NS create, should have created one RoleBinding, but created %v: %#v", len(client.objs), client.objs)) + } + + for key, _ := range client.objs { + if key.Namespace != targetNamespace { + t.Fatal(fmt.Sprintf("After NS create, should have created RoleBinding in %v namespace, but created in %v namespace", targetNamespace, key.Namespace)) + } + } +} + +// Apply the role bindings to a namespace that does not exist. +func TestTargetNamespacesGotDeleted(t *testing.T) { + targetNamespace1 := "fred" + targetNamespace2 := "george" + activeNamespace1 := "fred" + activeNamespace2 := "george" + + k := kabanerov1alpha2.Kabanero{ + ObjectMeta: metav1.ObjectMeta{Name: "kabanero", Namespace: "kabanero"}, + Spec: kabanerov1alpha2.KabaneroSpec{ + TargetNamespaces: []string{targetNamespace1, targetNamespace2}, + }, + Status: kabanerov1alpha2.KabaneroStatus { + TargetNamespaces: kabanerov1alpha2.TargetNamespaceStatus { + Namespaces: []string{activeNamespace1, activeNamespace2}, + Ready: "True", + }, + }, + } + + // Set up pre-existing objects + existingNamespaces := make(map[string]bool) + existingNamespaces[activeNamespace1] = true + existingRoleBinding := client.ObjectKey{Name: "kabanero-pipeline-deploy-rolebinding", Namespace: activeNamespace1} + existingRoleBindings := make(map[client.ObjectKey]bool) + existingRoleBindings[existingRoleBinding] = true + client := targetnamespaceTestClient{existingRoleBindings, existingNamespaces} + + err := reconcileTargetNamespaces(context.TODO(), &k, client, nslog) + + if err == nil { + t.Fatal("Did not return an error, but should have because namespace \"george\" does not exist") + } + + // Make sure the kabanero status was not updated with the target namespace, + // since it did not exist. + if len(k.Status.TargetNamespaces.Namespaces) != 1 { + t.Fatal(fmt.Sprintf("Kabanero status should have 1 target namespace, but has %v: %v", len(k.Status.TargetNamespaces.Namespaces), k.Status.TargetNamespaces.Namespaces)) + } + + if k.Status.TargetNamespaces.Namespaces[0] != "fred" { + t.Fatal(fmt.Sprintf("Kabanero status target namespace is not \"fred\", but is %v", k.Status.TargetNamespaces.Namespaces[0])) + } + + if k.Status.TargetNamespaces.Ready != "False" { + t.Fatal(fmt.Sprintf("Kabanero target namespace status is not False: %v", k.Status.TargetNamespaces.Ready)) + } + + if len(k.Status.TargetNamespaces.Message) == 0 { + t.Fatal("Kabanero target namespace status contains no error message") + } + + // Make sure the RoleBinding map got cleared. + if len(client.objs) != 1 { + t.Fatal(fmt.Sprintf("Should have created 1 RoleBindings, but created %v: %#v", len(client.objs), client.objs)) + } +} + +// Test callout from finalizer +func TestCleanupTargetNamespaces(t *testing.T) { + targetNamespace := "fred" + k := kabanerov1alpha2.Kabanero{ + ObjectMeta: metav1.ObjectMeta{Name: "kabanero", Namespace: "kabanero"}, + Spec: kabanerov1alpha2.KabaneroSpec{ + TargetNamespaces: []string{targetNamespace}, + }, + Status: kabanerov1alpha2.KabaneroStatus { + TargetNamespaces: kabanerov1alpha2.TargetNamespaceStatus { + Namespaces: []string{targetNamespace}, + Ready: "True", + }, + }, + } + + // Set up pre-existing objects + existingNamespaces := make(map[string]bool) + existingNamespaces[targetNamespace] = true + existingRoleBinding := client.ObjectKey{Name: "kabanero-pipeline-deploy-rolebinding", Namespace: targetNamespace} + existingRoleBindings := make(map[client.ObjectKey]bool) + existingRoleBindings[existingRoleBinding] = true + client := targetnamespaceTestClient{existingRoleBindings, existingNamespaces} + + err := cleanupTargetNamespaces(context.TODO(), &k, client) + + if err != nil { + t.Fatal("Returned error: " + err.Error()) + } + + if len(existingRoleBindings) != 0 { + t.Fatal(fmt.Sprintf("There were %v bindings left in the map after cleanup: %#v", len(existingRoleBindings), existingRoleBindings)) + } +}