diff --git a/chart/kube-arangodb/templates/ml/cluster-role-binding.yaml b/chart/kube-arangodb/templates/ml-operator/cluster-role-binding.yaml similarity index 100% rename from chart/kube-arangodb/templates/ml/cluster-role-binding.yaml rename to chart/kube-arangodb/templates/ml-operator/cluster-role-binding.yaml diff --git a/chart/kube-arangodb/templates/ml/cluster-role.yaml b/chart/kube-arangodb/templates/ml-operator/cluster-role.yaml similarity index 100% rename from chart/kube-arangodb/templates/ml/cluster-role.yaml rename to chart/kube-arangodb/templates/ml-operator/cluster-role.yaml diff --git a/chart/kube-arangodb/templates/ml/role-binding.yaml b/chart/kube-arangodb/templates/ml-operator/role-binding.yaml similarity index 100% rename from chart/kube-arangodb/templates/ml/role-binding.yaml rename to chart/kube-arangodb/templates/ml-operator/role-binding.yaml diff --git a/chart/kube-arangodb/templates/ml/role.yaml b/chart/kube-arangodb/templates/ml-operator/role.yaml similarity index 87% rename from chart/kube-arangodb/templates/ml/role.yaml rename to chart/kube-arangodb/templates/ml-operator/role.yaml index a5dd45353..45e260e41 100644 --- a/chart/kube-arangodb/templates/ml/role.yaml +++ b/chart/kube-arangodb/templates/ml-operator/role.yaml @@ -34,10 +34,17 @@ rules: - "get" - "list" - "watch" + - apiGroups: + - "rbac.authorization.k8s.io/v1" + resources: + - "roles" + - "rolebindings" + verbs: ["*"] - apiGroups: [""] resources: - - "secrets" - "pods" + - "secrets" + - "serviceaccounts" verbs: ["*"] {{- end }} {{- end }} \ No newline at end of file diff --git a/docs/api/ArangoMLExtension.V1Alpha1.md b/docs/api/ArangoMLExtension.V1Alpha1.md index adf256690..7ccca8629 100644 --- a/docs/api/ArangoMLExtension.V1Alpha1.md +++ b/docs/api/ArangoMLExtension.V1Alpha1.md @@ -76,7 +76,7 @@ PullSecrets define Secrets used to pull Image from registry ### .spec.storage.name -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L32) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) Name of the object @@ -84,7 +84,7 @@ Name of the object ### .spec.storage.namespace -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L35) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) Namespace of the object. Should default to the namespace of the parent object @@ -92,7 +92,7 @@ Namespace of the object. Should default to the namespace of the parent object ### .spec.storage.uid -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L38) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) UID keeps the information about object UID @@ -100,7 +100,7 @@ UID keeps the information about object UID ### .status.conditions -Type: `api.Conditions` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/ml/v1alpha1/extension_status.go#L28) +Type: `api.Conditions` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/ml/v1alpha1/extension_status.go#L31) Conditions specific to the entire extension @@ -124,7 +124,7 @@ ArangoPipeDatabase define Database name to be used as MetadataService Backend ### .status.metadataService.secret.name -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L32) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) Name of the object @@ -132,7 +132,7 @@ Name of the object ### .status.metadataService.secret.namespace -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L35) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) Namespace of the object. Should default to the namespace of the parent object @@ -140,7 +140,127 @@ Namespace of the object. Should default to the namespace of the parent object ### .status.metadataService.secret.uid -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L38) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) + +UID keeps the information about object UID + +*** + +### .status.serviceAccount.cluster.binding.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) + +Name of the object + +*** + +### .status.serviceAccount.cluster.binding.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .status.serviceAccount.cluster.binding.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) + +UID keeps the information about object UID + +*** + +### .status.serviceAccount.cluster.role.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) + +Name of the object + +*** + +### .status.serviceAccount.cluster.role.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .status.serviceAccount.cluster.role.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) + +UID keeps the information about object UID + +*** + +### .status.serviceAccount.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) + +Name of the object + +*** + +### .status.serviceAccount.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .status.serviceAccount.namespaced.binding.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) + +Name of the object + +*** + +### .status.serviceAccount.namespaced.binding.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .status.serviceAccount.namespaced.binding.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) + +UID keeps the information about object UID + +*** + +### .status.serviceAccount.namespaced.role.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) + +Name of the object + +*** + +### .status.serviceAccount.namespaced.role.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .status.serviceAccount.namespaced.role.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) + +UID keeps the information about object UID + +*** + +### .status.serviceAccount.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) UID keeps the information about object UID diff --git a/docs/api/ArangoMLStorage.V1Alpha1.md b/docs/api/ArangoMLStorage.V1Alpha1.md index fe1fce46c..b065d0ae6 100644 --- a/docs/api/ArangoMLStorage.V1Alpha1.md +++ b/docs/api/ArangoMLStorage.V1Alpha1.md @@ -14,7 +14,7 @@ Default Value: `false` ### .spec.backend.s3.caSecret.name -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L32) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) Name of the object @@ -22,7 +22,7 @@ Name of the object ### .spec.backend.s3.caSecret.namespace -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L35) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) Namespace of the object. Should default to the namespace of the parent object @@ -30,7 +30,7 @@ Namespace of the object. Should default to the namespace of the parent object ### .spec.backend.s3.caSecret.uid -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L38) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) UID keeps the information about object UID @@ -38,7 +38,7 @@ UID keeps the information about object UID ### .spec.backend.s3.credentialsSecret.name -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L32) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L46) Name of the object @@ -46,7 +46,7 @@ Name of the object ### .spec.backend.s3.credentialsSecret.namespace -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L35) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L49) Namespace of the object. Should default to the namespace of the parent object @@ -54,7 +54,7 @@ Namespace of the object. Should default to the namespace of the parent object ### .spec.backend.s3.credentialsSecret.uid -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L38) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.35/pkg/apis/shared/v1/object.go#L52) UID keeps the information about object UID diff --git a/pkg/apis/ml/v1alpha1/extension_conditions.go b/pkg/apis/ml/v1alpha1/extension_conditions.go index a5af8f0ff..286032937 100644 --- a/pkg/apis/ml/v1alpha1/extension_conditions.go +++ b/pkg/apis/ml/v1alpha1/extension_conditions.go @@ -27,5 +27,6 @@ const ( ExtensionDeploymentFoundCondition api.ConditionType = "DeploymentFound" ExtensionBootstrapCompletedCondition api.ConditionType = "BootstrapCompleted" ExtensionMetadataServiceValidCondition api.ConditionType = "MetadataServiceValid" + ExtensionServiceAccountReadyCondition api.ConditionType = "ServiceAccountReady" LicenseValidCondition api.ConditionType = "LicenseValid" ) diff --git a/pkg/apis/ml/v1alpha1/extension_status.go b/pkg/apis/ml/v1alpha1/extension_status.go index 08d0b1967..ab92e2987 100644 --- a/pkg/apis/ml/v1alpha1/extension_status.go +++ b/pkg/apis/ml/v1alpha1/extension_status.go @@ -20,7 +20,10 @@ package v1alpha1 -import api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" +import ( + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" +) type ArangoMLExtensionStatus struct { // Conditions specific to the entire extension @@ -29,4 +32,7 @@ type ArangoMLExtensionStatus struct { // MetadataService keeps the MetadataService configuration MetadataService *ArangoMLExtensionStatusMetadataService `json:"metadataService,omitempty"` + + // ServiceAccount keeps the information about ServiceAccount + ServiceAccount *shared.ServiceAccount `json:"serviceAccount,omitempty"` } diff --git a/pkg/apis/ml/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/ml/v1alpha1/zz_generated.deepcopy.go index 46f1bdcb2..30b061d3a 100644 --- a/pkg/apis/ml/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/ml/v1alpha1/zz_generated.deepcopy.go @@ -418,6 +418,11 @@ func (in *ArangoMLExtensionStatus) DeepCopyInto(out *ArangoMLExtensionStatus) { *out = new(ArangoMLExtensionStatusMetadataService) (*in).DeepCopyInto(*out) } + if in.ServiceAccount != nil { + in, out := &in.ServiceAccount, &out.ServiceAccount + *out = new(sharedv1.ServiceAccount) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/apis/shared/v1/object.go b/pkg/apis/shared/v1/object.go index 1527a126b..bb978fdca 100644 --- a/pkg/apis/shared/v1/object.go +++ b/pkg/apis/shared/v1/object.go @@ -25,8 +25,22 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/util" ) +func NewObject(object meta.Object) Object { + var n Object + + n.Name = object.GetName() + n.UID = util.NewType(object.GetUID()) + + if ns := object.GetNamespace(); ns != "" { + n.Namespace = util.NewType(ns) + } + + return n +} + type Object struct { // Name of the object Name string `json:"name"` @@ -71,6 +85,30 @@ func (o *Object) GetUID() types.UID { return "" } +func (o *Object) Equals(obj meta.Object) bool { + if o == nil { + return false + } + + if o.Name != obj.GetName() { + return false + } + + if n := o.Namespace; n != nil { + if *n != obj.GetNamespace() { + return false + } + } + + if n := o.UID; n != nil { + if *n != obj.GetUID() { + return false + } + } + + return true +} + func (o *Object) Validate() error { if o == nil { o = &Object{} diff --git a/pkg/apis/shared/v1/service_account.go b/pkg/apis/shared/v1/service_account.go new file mode 100644 index 000000000..bcd1c8eeb --- /dev/null +++ b/pkg/apis/shared/v1/service_account.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v1 + +type ServiceAccount struct { + // Object keeps the reference to the ServiceAccount + *Object `json:",inline"` + + // Namespaced keeps the reference to core.Role objects + Namespaced *ServiceAccountRole `json:"namespaced,omitempty"` + + // Cluster keeps the reference to core.ClusterRole objects + Cluster *ServiceAccountRole `json:"cluster,omitempty"` +} + +type ServiceAccountRole struct { + // Role keeps the reference to the Kubernetes Role + Role *Object `json:"role,omitempty"` + + // Binding keeps the reference to the Kubernetes Binding + Binding *Object `json:"binding,omitempty"` +} diff --git a/pkg/apis/shared/v1/zz_generated.deepcopy.go b/pkg/apis/shared/v1/zz_generated.deepcopy.go index 65379225e..02d8e5454 100644 --- a/pkg/apis/shared/v1/zz_generated.deepcopy.go +++ b/pkg/apis/shared/v1/zz_generated.deepcopy.go @@ -127,3 +127,60 @@ func (in *Resources) DeepCopy() *Resources { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccount) DeepCopyInto(out *ServiceAccount) { + *out = *in + if in.Object != nil { + in, out := &in.Object, &out.Object + *out = new(Object) + (*in).DeepCopyInto(*out) + } + if in.Namespaced != nil { + in, out := &in.Namespaced, &out.Namespaced + *out = new(ServiceAccountRole) + (*in).DeepCopyInto(*out) + } + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(ServiceAccountRole) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccount. +func (in *ServiceAccount) DeepCopy() *ServiceAccount { + if in == nil { + return nil + } + out := new(ServiceAccount) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountRole) DeepCopyInto(out *ServiceAccountRole) { + *out = *in + if in.Role != nil { + in, out := &in.Role, &out.Role + *out = new(Object) + (*in).DeepCopyInto(*out) + } + if in.Binding != nil { + in, out := &in.Binding, &out.Binding + *out = new(Object) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountRole. +func (in *ServiceAccountRole) DeepCopy() *ServiceAccountRole { + if in == nil { + return nil + } + out := new(ServiceAccountRole) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/util/context.go b/pkg/util/context.go index 36ee4f867..a4da3761d 100644 --- a/pkg/util/context.go +++ b/pkg/util/context.go @@ -24,6 +24,10 @@ import ( "context" "time" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" "github.com/arangodb/kube-arangodb/pkg/util/globals" ) @@ -48,3 +52,25 @@ func WithContextTimeoutP2A2[P1, P2, A1, A2 interface{}](ctx context.Context, tim return f(nCtx, a1, a2) } + +type PatchInterface[P1 meta.Object] interface { + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts meta.PatchOptions, subresources ...string) (P1, error) +} + +func WithKubernetesPatch[P1 meta.Object](ctx context.Context, obj string, client PatchInterface[P1], p ...patch.Item) (P1, error) { + if len(p) == 0 { + return Default[P1](), nil + } + + parser := patch.Patch(p) + + data, err := parser.Marshal() + if err != nil { + return Default[P1](), err + } + + nCtx, c := context.WithTimeout(ctx, globals.GetGlobals().Timeouts().Kubernetes().Get()) + defer c() + + return client.Patch(nCtx, obj, types.JSONPatchType, data, meta.PatchOptions{}) +} diff --git a/pkg/util/k8sutil/helpers/service_account.go b/pkg/util/k8sutil/helpers/service_account.go new file mode 100644 index 000000000..bb0dcedba --- /dev/null +++ b/pkg/util/k8sutil/helpers/service_account.go @@ -0,0 +1,385 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package helpers + +import ( + "context" + "fmt" + "strings" + + "github.com/dchest/uniuri" + core "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + operator "github.com/arangodb/kube-arangodb/pkg/operatorV2" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" +) + +func EnsureServiceAccount(ctx context.Context, client kubernetes.Interface, owner meta.OwnerReference, obj *sharedApi.ServiceAccount, name, namespace string, role, clusterRole []rbac.PolicyRule) (bool, error) { + if obj == nil { + return false, errors.Newf("Object reference cannot be nil") + } + + // Check if secret exists + if obj.Object != nil { + if sa, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.CoreV1().ServiceAccounts(namespace).Get, obj.Object.GetName(), meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + + obj.Object = nil + + return true, operator.Reconcile("SA is missing") + } else { + if !obj.Object.Equals(sa) { + // Invalid object + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.CoreV1().ServiceAccounts(namespace).Delete, name, meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Object.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Object = nil + + return true, operator.Reconcile("Removing SA") + } + } + } + + if obj.Object == nil { + if sa, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.CoreV1().ServiceAccounts(namespace).Create, &core.ServiceAccount{ + ObjectMeta: meta.ObjectMeta{ + OwnerReferences: []meta.OwnerReference{owner}, + + Name: fmt.Sprintf("%s-%s", name, strings.ToLower(uniuri.NewLen(6))), + Namespace: namespace, + }, + }, meta.CreateOptions{}); err != nil { + return false, err + } else { + obj.Object = util.NewType(sharedApi.NewObject(sa)) + return true, operator.Reconcile("Created SA") + } + } + + // ROLE + + if len(role) == 0 { + // Ensure role and binding is missing + if obj.Namespaced != nil { + if obj.Namespaced.Binding != nil { + // Remove binding + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().RoleBindings(namespace).Delete, obj.Namespaced.Binding.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Namespaced.Binding.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Namespaced.Binding = nil + return true, operator.Reconcile("Removing RB") + } + + if obj.Namespaced.Role != nil { + // Remove binding + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().Roles(namespace).Delete, obj.Namespaced.Role.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Namespaced.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Namespaced.Role = nil + return true, operator.Reconcile("Removing R") + } + + obj.Namespaced = nil + return true, operator.Reconcile("Removing Namespaced Handler") + } + } else { + // Create if required + if obj.Namespaced == nil || obj.Namespaced.Role == nil { + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().Roles(namespace).Create, &rbac.Role{ + ObjectMeta: meta.ObjectMeta{ + OwnerReferences: []meta.OwnerReference{owner}, + + Name: fmt.Sprintf("%s-%s", name, strings.ToLower(uniuri.NewLen(6))), + Namespace: namespace, + }, + Rules: role, + }, meta.CreateOptions{}); err != nil { + return false, err + } else { + if obj.Namespaced == nil { + obj.Namespaced = &sharedApi.ServiceAccountRole{} + } + obj.Namespaced.Role = util.NewType(sharedApi.NewObject(r)) + return true, operator.Reconcile("Created R") + } + } + + if obj.Namespaced == nil || obj.Namespaced.Binding == nil { + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().RoleBindings(namespace).Create, &rbac.RoleBinding{ + ObjectMeta: meta.ObjectMeta{ + OwnerReferences: []meta.OwnerReference{owner}, + + Name: fmt.Sprintf("%s-%s", name, strings.ToLower(uniuri.NewLen(6))), + Namespace: namespace, + }, + RoleRef: rbac.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: obj.Namespaced.Role.GetName(), + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: obj.Object.GetName(), + Namespace: namespace, + }, + }, + }, meta.CreateOptions{}); err != nil { + return false, err + } else { + if obj.Namespaced == nil { + obj.Namespaced = &sharedApi.ServiceAccountRole{} + } + obj.Namespaced.Binding = util.NewType(sharedApi.NewObject(r)) + return true, operator.Reconcile("Created RB") + } + } + + // Both object are nil, lets validate aspects + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().Roles(namespace).Get, obj.Namespaced.Role.GetName(), meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + + obj.Namespaced.Role = nil + return true, operator.Reconcile("Missing R") + } else { + if !obj.Namespaced.Role.Equals(r) { + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().Roles(namespace).Delete, obj.Namespaced.Role.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Namespaced.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Namespaced.Role = nil + return true, operator.Reconcile("Recreate R") + } + + if !equality.Semantic.DeepEqual(r.Rules, role) { + // There is change in the roles + if _, err := util.WithKubernetesPatch[*rbac.Role](ctx, obj.Namespaced.Role.GetName(), client.RbacV1().Roles(namespace), patch.ItemReplace(patch.NewPath("rules"), role)); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + } + + return false, operator.Reconcile("Refresh R") + } + } + + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().RoleBindings(namespace).Get, obj.Namespaced.Binding.GetName(), meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + + obj.Namespaced.Role = nil + return true, operator.Reconcile("Missing RB") + } else { + if !obj.Namespaced.Binding.Equals(r) { + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().RoleBindings(namespace).Delete, obj.Namespaced.Binding.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Namespaced.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Namespaced.Role = nil + return true, operator.Reconcile("Recreate RB") + } + } + } + + // CLUSTER ROLE + + if len(clusterRole) == 0 { + // Ensure role and binding is missing + if obj.Cluster != nil { + if obj.Cluster.Binding != nil { + // Remove binding + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().ClusterRoleBindings().Delete, obj.Cluster.Binding.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Cluster.Binding.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Cluster.Binding = nil + return true, operator.Reconcile("Removing CRB") + } + + if obj.Cluster.Role != nil { + // Remove binding + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().ClusterRoles().Delete, obj.Cluster.Role.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Cluster.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Cluster.Role = nil + return true, operator.Reconcile("Removing CR") + } + + obj.Cluster = nil + return true, operator.Reconcile("Removing Cluster Handler") + } + } else { + // Create if required + if obj.Cluster == nil || obj.Cluster.Role == nil { + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().ClusterRoles().Create, &rbac.ClusterRole{ + ObjectMeta: meta.ObjectMeta{ + OwnerReferences: []meta.OwnerReference{owner}, + + Name: fmt.Sprintf("%s-%s", name, strings.ToLower(uniuri.NewLen(6))), + }, + Rules: clusterRole, + }, meta.CreateOptions{}); err != nil { + return false, err + } else { + if obj.Cluster == nil { + obj.Cluster = &sharedApi.ServiceAccountRole{} + } + obj.Cluster.Role = util.NewType(sharedApi.NewObject(r)) + return true, operator.Reconcile("Created CR") + } + } + + if obj.Cluster == nil || obj.Cluster.Binding == nil { + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().ClusterRoleBindings().Create, &rbac.ClusterRoleBinding{ + ObjectMeta: meta.ObjectMeta{ + OwnerReferences: []meta.OwnerReference{owner}, + + Name: fmt.Sprintf("%s-%s", name, strings.ToLower(uniuri.NewLen(6))), + }, + RoleRef: rbac.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: obj.Cluster.Role.GetName(), + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: obj.Object.GetName(), + Namespace: namespace, + }, + }, + }, meta.CreateOptions{}); err != nil { + return false, err + } else { + if obj.Cluster == nil { + obj.Cluster = &sharedApi.ServiceAccountRole{} + } + obj.Cluster.Binding = util.NewType(sharedApi.NewObject(r)) + return true, operator.Reconcile("Created CRB") + } + } + + // Both object are nil, lets validate aspects + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().ClusterRoles().Get, obj.Cluster.Role.GetName(), meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + + obj.Cluster.Role = nil + return true, operator.Reconcile("Missing CR") + } else { + if !obj.Cluster.Role.Equals(r) { + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().ClusterRoles().Delete, obj.Cluster.Role.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Cluster.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Cluster.Role = nil + return true, operator.Reconcile("Recreate CR") + } + + if !equality.Semantic.DeepEqual(r.Rules, clusterRole) { + // There is change in the roles + if _, err := util.WithKubernetesPatch[*rbac.ClusterRole](ctx, obj.Cluster.Role.GetName(), client.RbacV1().ClusterRoles(), patch.ItemReplace(patch.NewPath("rules"), clusterRole)); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + } + + return false, operator.Reconcile("Refresh CR") + } + } + + if r, err := util.WithKubernetesContextTimeoutP2A2(ctx, client.RbacV1().ClusterRoleBindings().Get, obj.Cluster.Binding.GetName(), meta.GetOptions{}); err != nil { + if !kerrors.IsNotFound(err) { + return false, err + } + + obj.Cluster.Role = nil + return true, operator.Reconcile("Missing CRB") + } else { + if !obj.Cluster.Binding.Equals(r) { + if err := util.WithKubernetesContextTimeoutP1A2(ctx, client.RbacV1().ClusterRoleBindings().Delete, obj.Cluster.Binding.GetName(), meta.DeleteOptions{ + Preconditions: meta.NewUIDPreconditions(string(obj.Cluster.Role.GetUID())), + }); err != nil { + if !kerrors.IsNotFound(err) && !kerrors.IsConflict(err) { + return false, err + } + } + + obj.Cluster.Role = nil + return true, operator.Reconcile("Recreate CRB") + } + } + } + + return false, nil +} diff --git a/pkg/util/k8sutil/helpers/service_account_test.go b/pkg/util/k8sutil/helpers/service_account_test.go new file mode 100644 index 000000000..72aeca63b --- /dev/null +++ b/pkg/util/k8sutil/helpers/service_account_test.go @@ -0,0 +1,298 @@ +// +// DISCLAIMER +// +// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package helpers + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + rbac "k8s.io/api/rbac/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func Test_ServiceAccount_Roles(t *testing.T) { + k := fake.NewSimpleClientset() + + var obj sharedApi.ServiceAccount + + t.Run("PreCheck", func(t *testing.T) { + require.Nil(t, obj.Object) + }) + + t.Run("Create SA without any roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, nil) + })) + + require.NotNil(t, obj.Object) + require.Nil(t, obj.Namespaced) + require.Nil(t, obj.Cluster) + }) + + t.Run("Create SA with roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, []rbac.PolicyRule{ + { + Resources: []string{"*"}, + }, + }, nil) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Namespaced) + require.NotNil(t, obj.Namespaced.Binding) + require.NotNil(t, obj.Namespaced.Role) + require.Nil(t, obj.Cluster) + + sa := tests.NewMetaObject[*rbac.Role](t, tests.FakeNamespace, obj.Namespaced.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 1) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "*") + }) + + t.Run("Create SA with updated roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, []rbac.PolicyRule{ + { + Resources: []string{"DATA"}, + }, + }, nil) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Namespaced) + require.NotNil(t, obj.Namespaced.Binding) + require.NotNil(t, obj.Namespaced.Role) + require.Nil(t, obj.Cluster) + + sa := tests.NewMetaObject[*rbac.Role](t, tests.FakeNamespace, obj.Namespaced.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 1) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "DATA") + }) + + t.Run("Create SA with multiple roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, []rbac.PolicyRule{ + { + Resources: []string{"DATA"}, + }, + { + Resources: []string{"*"}, + }, + }, nil) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Namespaced) + require.NotNil(t, obj.Namespaced.Binding) + require.NotNil(t, obj.Namespaced.Role) + require.Nil(t, obj.Cluster) + + sa := tests.NewMetaObject[*rbac.Role](t, tests.FakeNamespace, obj.Namespaced.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 2) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "DATA") + }) + + t.Run("Create SA with updated multiple roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, []rbac.PolicyRule{ + { + Resources: []string{"*"}, + }, + { + Resources: []string{"DATA"}, + }, + }, nil) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Namespaced) + require.NotNil(t, obj.Namespaced.Binding) + require.NotNil(t, obj.Namespaced.Role) + require.Nil(t, obj.Cluster) + + sa := tests.NewMetaObject[*rbac.Role](t, tests.FakeNamespace, obj.Namespaced.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 2) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "*") + }) + + t.Run("Remove SA Roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, nil) + })) + + require.NotNil(t, obj.Object) + require.Nil(t, obj.Namespaced) + require.Nil(t, obj.Cluster) + }) +} + +func Test_ServiceAccount_ClusterRoles(t *testing.T) { + k := fake.NewSimpleClientset() + + var obj sharedApi.ServiceAccount + + t.Run("PreCheck", func(t *testing.T) { + require.Nil(t, obj.Object) + }) + + t.Run("Create SA without any roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, nil) + })) + + require.NotNil(t, obj.Object) + require.Nil(t, obj.Cluster) + require.Nil(t, obj.Namespaced) + }) + + t.Run("Create SA with roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, []rbac.PolicyRule{ + { + Resources: []string{"*"}, + }, + }) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Cluster) + require.NotNil(t, obj.Cluster.Binding) + require.NotNil(t, obj.Cluster.Role) + require.Nil(t, obj.Namespaced) + + sa := tests.NewMetaObject[*rbac.ClusterRole](t, tests.FakeNamespace, obj.Cluster.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 1) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "*") + }) + + t.Run("Create SA with updated roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, []rbac.PolicyRule{ + { + Resources: []string{"DATA"}, + }, + }) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Cluster) + require.NotNil(t, obj.Cluster.Binding) + require.NotNil(t, obj.Cluster.Role) + require.Nil(t, obj.Namespaced) + + sa := tests.NewMetaObject[*rbac.ClusterRole](t, tests.FakeNamespace, obj.Cluster.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 1) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "DATA") + }) + + t.Run("Create SA with multiple roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, []rbac.PolicyRule{ + { + Resources: []string{"DATA"}, + }, + { + Resources: []string{"*"}, + }, + }) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Cluster) + require.NotNil(t, obj.Cluster.Binding) + require.NotNil(t, obj.Cluster.Role) + require.Nil(t, obj.Namespaced) + + sa := tests.NewMetaObject[*rbac.ClusterRole](t, tests.FakeNamespace, obj.Cluster.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 2) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "DATA") + }) + + t.Run("Create SA with updated multiple roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, []rbac.PolicyRule{ + { + Resources: []string{"*"}, + }, + { + Resources: []string{"DATA"}, + }, + }) + })) + + require.NotNil(t, obj.Object) + require.NotNil(t, obj.Cluster) + require.NotNil(t, obj.Cluster.Binding) + require.NotNil(t, obj.Cluster.Role) + require.Nil(t, obj.Namespaced) + + sa := tests.NewMetaObject[*rbac.ClusterRole](t, tests.FakeNamespace, obj.Cluster.Role.GetName()) + + tests.RefreshObjects(t, k, nil, &sa) + + require.Len(t, sa.Rules, 2) + require.Len(t, sa.Rules[0].Resources, 1) + require.Equal(t, sa.Rules[0].Resources[0], "*") + }) + + t.Run("Remove SA Roles", func(t *testing.T) { + require.NoError(t, tests.HandleFunc(func(ctx context.Context) (bool, error) { + return EnsureServiceAccount(ctx, k, meta.OwnerReference{}, &obj, "test", tests.FakeNamespace, nil, nil) + })) + + require.NotNil(t, obj.Object) + require.Nil(t, obj.Cluster) + require.Nil(t, obj.Namespaced) + }) +} diff --git a/pkg/util/tests/kubernetes.go b/pkg/util/tests/kubernetes.go index 691cf011d..e6f064453 100644 --- a/pkg/util/tests/kubernetes.go +++ b/pkg/util/tests/kubernetes.go @@ -29,6 +29,7 @@ import ( "github.com/stretchr/testify/require" batch "k8s.io/api/batch/v1" core "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/kubernetes" @@ -46,6 +47,27 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" ) +type handleFunc struct { + in func(ctx context.Context) (bool, error) +} + +func (h handleFunc) Name() string { + return "mock" +} + +func (h handleFunc) Handle(ctx context.Context, item operation.Item) error { + _, err := h.in(ctx) + return err +} + +func (h handleFunc) CanBeHandled(item operation.Item) bool { + return true +} + +func HandleFunc(in func(ctx context.Context) (bool, error)) error { + return Handle(handleFunc{in: in}, operation.Item{}) +} + func Handle(handler operator.Handler, item operation.Item) error { return HandleWithMax(handler, item, 128) } @@ -80,17 +102,23 @@ func CreateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.BatchV1().Jobs(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **core.Pod: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().Pods(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) case **core.Secret: require.NotNil(t, v) vl := *v _, err := k8s.CoreV1().Secrets(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) - case **core.Pod: + case **core.ServiceAccount: require.NotNil(t, v) vl := *v - _, err := k8s.CoreV1().Pods(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + _, err := k8s.CoreV1().ServiceAccounts(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) case **api.ArangoDeployment: require.NotNil(t, v) @@ -122,6 +150,30 @@ func CreateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := arango.MlV1alpha1().ArangoMLStorages(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **rbac.ClusterRole: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().ClusterRoles().Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) + case **rbac.ClusterRoleBinding: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().ClusterRoleBindings().Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) + case **rbac.Role: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().Roles(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) + case **rbac.RoleBinding: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().RoleBindings(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } @@ -153,6 +205,12 @@ func UpdateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.CoreV1().Secrets(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) require.NoError(t, err) + case **core.ServiceAccount: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().ServiceAccounts(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) case **api.ArangoDeployment: require.NotNil(t, v) @@ -183,6 +241,30 @@ func UpdateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := arango.MlV1alpha1().ArangoMLStorages(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) require.NoError(t, err) + case **rbac.ClusterRole: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().ClusterRoles().Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) + case **rbac.ClusterRoleBinding: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().ClusterRoleBindings().Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) + case **rbac.Role: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().Roles(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) + case **rbac.RoleBinding: + require.NotNil(t, v) + + vl := *v + _, err := k8s.RbacV1().RoleBindings(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } @@ -241,6 +323,21 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **core.ServiceAccount: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.CoreV1().ServiceAccounts(vl.GetNamespace()).Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } case **api.ArangoDeployment: require.NotNil(t, v) @@ -316,6 +413,66 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **rbac.ClusterRole: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.RbacV1().ClusterRoles().Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } + case **rbac.ClusterRoleBinding: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.RbacV1().ClusterRoleBindings().Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } + case **rbac.Role: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.RbacV1().Roles(vl.GetNamespace()).Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } + case **rbac.RoleBinding: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.RbacV1().RoleBindings(vl.GetNamespace()).Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } @@ -344,6 +501,12 @@ func SetMetaBasedOnType(t *testing.T, object meta.Object) { v.SetSelfLink(fmt.Sprintf("/api/v1/secrets/%s/%s", object.GetNamespace(), object.GetName())) + case *core.ServiceAccount: + v.Kind = "ServiceAccount" + v.APIVersion = "v1" + v.SetSelfLink(fmt.Sprintf("/api/v1/serviceaccounts/%s/%s", + object.GetNamespace(), + object.GetName())) case *api.ArangoDeployment: v.Kind = deployment.ArangoDeploymentResourceKind v.APIVersion = api.SchemeGroupVersion.String() @@ -384,6 +547,30 @@ func SetMetaBasedOnType(t *testing.T, object meta.Object) { ml.ArangoMLStorageResourcePlural, object.GetNamespace(), object.GetName())) + case *rbac.ClusterRole: + v.Kind = "ClusterRole" + v.APIVersion = "rbac.authorization.k8s.io/v1" + v.SetSelfLink(fmt.Sprintf("/api/rbac.authorization.k8s.io/v1/clusterroles/%s/%s", + object.GetNamespace(), + object.GetName())) + case *rbac.ClusterRoleBinding: + v.Kind = "ClusterRoleBinding" + v.APIVersion = "rbac.authorization.k8s.io/v1" + v.SetSelfLink(fmt.Sprintf("/api/rbac.authorization.k8s.io/v1/clusterrolebingings/%s/%s", + object.GetNamespace(), + object.GetName())) + case *rbac.Role: + v.Kind = "Role" + v.APIVersion = "rbac.authorization.k8s.io/v1" + v.SetSelfLink(fmt.Sprintf("/api/rbac.authorization.k8s.io/v1/roles/%s/%s", + object.GetNamespace(), + object.GetName())) + case *rbac.RoleBinding: + v.Kind = "RoleBinding" + v.APIVersion = "rbac.authorization.k8s.io/v1" + v.SetSelfLink(fmt.Sprintf("/api/rbac.authorization.k8s.io/v1/rolebingings/%s/%s", + object.GetNamespace(), + object.GetName())) default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } @@ -398,7 +585,9 @@ func NewMetaObject[T meta.Object](t *testing.T, namespace, name string, mods ... reflect.ValueOf(&obj).Elem().Set(newObj) } - obj.SetNamespace(namespace) + if IsNamespaced(obj) { + obj.SetNamespace(namespace) + } obj.SetName(name) obj.SetUID(uuid.NewUUID()) @@ -411,6 +600,15 @@ func NewMetaObject[T meta.Object](t *testing.T, namespace, name string, mods ... return obj } +func IsNamespaced(in meta.Object) bool { + switch in.(type) { + case *rbac.ClusterRole, *rbac.ClusterRoleBinding: + return false + default: + return true + } +} + func NewItem(t *testing.T, o operation.Operation, object meta.Object) operation.Item { item := operation.Item{ Operation: o, @@ -432,6 +630,10 @@ func NewItem(t *testing.T, o operation.Operation, object meta.Object) operation. item.Group = "" item.Version = "v1" item.Kind = "Secret" + case *core.ServiceAccount: + item.Group = "" + item.Version = "v1" + item.Kind = "ServiceAccount" case *api.ArangoDeployment: item.Group = deployment.ArangoDeploymentGroupName item.Version = api.ArangoDeploymentVersion @@ -452,6 +654,22 @@ func NewItem(t *testing.T, o operation.Operation, object meta.Object) operation. item.Group = ml.ArangoMLGroupName item.Version = mlApi.ArangoMLVersion item.Kind = ml.ArangoMLStorageResourceKind + case *rbac.ClusterRole: + item.Group = "rbac.authorization.k8s.io" + item.Version = "v1" + item.Kind = "ClusterRole" + case *rbac.ClusterRoleBinding: + item.Group = "rbac.authorization.k8s.io" + item.Version = "v1" + item.Kind = "ClusterRoleBinding" + case *rbac.Role: + item.Group = "rbac.authorization.k8s.io" + item.Version = "v1" + item.Kind = "Role" + case *rbac.RoleBinding: + item.Group = "rbac.authorization.k8s.io" + item.Version = "v1" + item.Kind = "RoleBinding" default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } diff --git a/pkg/util/tests/kubernetes_test.go b/pkg/util/tests/kubernetes_test.go index 3123660a1..d9433c177 100644 --- a/pkg/util/tests/kubernetes_test.go +++ b/pkg/util/tests/kubernetes_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/require" batch "k8s.io/api/batch/v1" core "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" backupApi "github.com/arangodb/kube-arangodb/pkg/apis/backup/v1" @@ -62,6 +63,11 @@ func Test_NewMetaObject(t *testing.T) { NewMetaObjectRun[*batch.Job](t) NewMetaObjectRun[*core.Pod](t) NewMetaObjectRun[*core.Secret](t) + NewMetaObjectRun[*core.ServiceAccount](t) + NewMetaObjectRun[*rbac.Role](t) + NewMetaObjectRun[*rbac.RoleBinding](t) + NewMetaObjectRun[*rbac.ClusterRole](t) + NewMetaObjectRun[*rbac.ClusterRoleBinding](t) NewMetaObjectRun[*api.ArangoDeployment](t) NewMetaObjectRun[*api.ArangoClusterSynchronization](t) NewMetaObjectRun[*backupApi.ArangoBackup](t)