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)