diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e386e5e..89c23ed80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - (Feature) (ML) Unify Images, Resources and Lifecycle - (Improvement) (ML) CronJob status update - (Improvement) (ML) Job Sidecar Shutdown +- (Feature) (ML) Handler for Extension StatefulSet and Service ## [1.2.35](https://github.com/arangodb/kube-arangodb/tree/1.2.35) (2023-11-06) - (Maintenance) Update go-driver to v1.6.0, update IsNotFound() checks diff --git a/chart/kube-arangodb/templates/ml-operator/role.yaml b/chart/kube-arangodb/templates/ml-operator/role.yaml index 1678e238c..9c1bfc8f9 100644 --- a/chart/kube-arangodb/templates/ml-operator/role.yaml +++ b/chart/kube-arangodb/templates/ml-operator/role.yaml @@ -46,10 +46,15 @@ rules: - "cronjobs" - "jobs" verbs: ["*"] + - apiGroups: ["apps"] + resources: + - "statefulsets" + verbs: ["*"] - apiGroups: [""] resources: - "pods" - "secrets" + - "services" - "serviceaccounts" verbs: ["*"] {{- end }} diff --git a/pkg/apis/ml/v1alpha1/extension_conditions.go b/pkg/apis/ml/v1alpha1/extension_conditions.go index 7a7c01395..5ecb04f51 100644 --- a/pkg/apis/ml/v1alpha1/extension_conditions.go +++ b/pkg/apis/ml/v1alpha1/extension_conditions.go @@ -28,6 +28,7 @@ const ( ExtensionBootstrapCompletedCondition api.ConditionType = "BootstrapCompleted" ExtensionMetadataServiceValidCondition api.ConditionType = "MetadataServiceValid" ExtensionServiceAccountReadyCondition api.ConditionType = "ServiceAccountReady" + ExtensionStatefulSetReadyCondition api.ConditionType = "ExtensionDeploymentReady" LicenseValidCondition api.ConditionType = "LicenseValid" CronJobSyncedCondition api.ConditionType = "CronJobSynced" ) diff --git a/pkg/handlers/backup/handler.go b/pkg/handlers/backup/handler.go index 0cab65d19..2e4a1da54 100644 --- a/pkg/handlers/backup/handler.go +++ b/pkg/handlers/backup/handler.go @@ -207,7 +207,7 @@ func (h *handler) getDeploymentMutex(namespace, deployment string) *sync.Mutex { } func (h *handler) Handle(_ context.Context, item operation.Item) error { - // Get Backup object. It also covers NotFound case + // Get object. It also covers NotFound case b, err := h.client.BackupV1().ArangoBackups(item.Namespace).Get(context.Background(), item.Name, meta.GetOptions{}) if err != nil { if apiErrors.IsNotFound(err) { diff --git a/pkg/handlers/policy/handler.go b/pkg/handlers/policy/handler.go index 2a7cd3646..cc9201d50 100644 --- a/pkg/handlers/policy/handler.go +++ b/pkg/handlers/policy/handler.go @@ -70,7 +70,7 @@ func (h *handler) Handle(_ context.Context, item operation.Item) error { return nil } - // Get Backup object. It also cover NotFound case + // Get object. It also cover NotFound case policy, err := h.client.BackupV1().ArangoBackupPolicies(item.Namespace).Get(context.Background(), item.Name, meta.GetOptions{}) if err != nil { return err diff --git a/pkg/license/license.go b/pkg/license/license.go index 32eed2940..f86bcea66 100644 --- a/pkg/license/license.go +++ b/pkg/license/license.go @@ -22,6 +22,7 @@ package license import ( "context" + "strconv" ) type Status int @@ -67,6 +68,28 @@ func (s Status) Valid() bool { func (s Status) Validate(feature Feature, subFeatures ...Feature) Status { return s } +func (s Status) String() string { + switch s { + case StatusMissing: + return "Missing" + case StatusInvalid: + return "Invalid" + case StatusInvalidSignature: + return "InvalidSignature" + case StatusNotYetValid: + return "NotYetValid" + case StatusNotAnymoreValid: + return "NotAnymoreValid" + case StatusFeatureNotEnabled: + return "FeatureNotEnabled" + case StatusFeatureExpired: + return "FeatureExpired" + case StatusValid: + return "Valid" + default: + return strconv.Itoa(int(s)) + } +} type Feature string diff --git a/pkg/util/helpers.go b/pkg/util/helpers.go new file mode 100644 index 000000000..0147c15a3 --- /dev/null +++ b/pkg/util/helpers.go @@ -0,0 +1,29 @@ +// +// 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 util + +// Ter implements a ternary operation: return cond ? a : b; +func Ter[T any](cond bool, a T, b T) T { + if cond { + return a + } + return b +} diff --git a/pkg/util/k8sutil/security_context.go b/pkg/util/k8sutil/security_context.go index 84dd58ba2..0f68479b0 100644 --- a/pkg/util/k8sutil/security_context.go +++ b/pkg/util/k8sutil/security_context.go @@ -24,7 +24,9 @@ import ( core "k8s.io/api/core/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/util" ) // CreateSecurityContext returns security context. @@ -37,3 +39,29 @@ func CreateSecurityContext(spec *api.ServerGroupSpecSecurityContext) *core.Secur func CreatePodSecurityContext(spec *api.ServerGroupSpecSecurityContext) *core.PodSecurityContext { return spec.NewPodSecurityContext(features.SecuredContainers().Enabled()) } + +func CreateSecurePodSecurityContext() *core.PodSecurityContext { + psc := &core.PodSecurityContext{ + RunAsUser: util.NewType[int64](shared.DefaultRunAsUser), + RunAsGroup: util.NewType[int64](shared.DefaultRunAsGroup), + RunAsNonRoot: util.NewType(true), + FSGroup: util.NewType[int64](shared.DefaultFSGroup), + } + + return psc +} + +func CreateDefaultSecurityContext() *core.SecurityContext { + r := &core.SecurityContext{ + RunAsUser: util.NewType[int64](shared.DefaultRunAsUser), + RunAsGroup: util.NewType[int64](shared.DefaultRunAsGroup), + RunAsNonRoot: util.NewType(true), + ReadOnlyRootFilesystem: util.NewType(true), + Capabilities: &core.Capabilities{ + Drop: []core.Capability{ + "ALL", + }, + }, + } + return r +} diff --git a/pkg/util/k8sutil/util.go b/pkg/util/k8sutil/util.go index 0dbb668f4..44e3f52af 100644 --- a/pkg/util/k8sutil/util.go +++ b/pkg/util/k8sutil/util.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-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. @@ -50,8 +50,12 @@ const ( LabelKeyArangoActive = "deployment.arangodb.com/active" // LabelValueArangoActive is the value of the label used to mark members as active. LabelValueArangoActive = "true" - // AppName is the fixed value for the "app" label + // LabelKeyArangoMLStatefulSet is the key of the label used to define k8s StatefulSet for ML Extension + LabelKeyArangoMLStatefulSet = "ml.arangodb.com/statefulset" + // AppName is the value for the "app" label AppName = "arangodb" + // AppArangoML is the value for the "app" label + AppArangoML = "arangoml" ) // AddOwnerRefToObject adds given owner reference to given object diff --git a/pkg/util/tests/kubernetes.go b/pkg/util/tests/kubernetes.go index 7c788e86e..eb967b64b 100644 --- a/pkg/util/tests/kubernetes.go +++ b/pkg/util/tests/kubernetes.go @@ -27,6 +27,7 @@ import ( "testing" "github.com/stretchr/testify/require" + apps "k8s.io/api/apps/v1" batch "k8s.io/api/batch/v1" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" @@ -120,12 +121,24 @@ func CreateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.CoreV1().Secrets(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **core.Service: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().Services(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) case **core.ServiceAccount: require.NotNil(t, v) vl := *v _, err := k8s.CoreV1().ServiceAccounts(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **apps.StatefulSet: + require.NotNil(t, v) + + vl := *v + _, err := k8s.AppsV1().StatefulSets(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) case **api.ArangoDeployment: require.NotNil(t, v) @@ -223,12 +236,23 @@ 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.Service: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().Services(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 **apps.StatefulSet: + require.NotNil(t, v) + vl := *v + _, err := k8s.AppsV1().StatefulSets(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) case **api.ArangoDeployment: require.NotNil(t, v) @@ -450,6 +474,21 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **core.Service: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.CoreV1().Services(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 **core.ServiceAccount: require.NotNil(t, v) @@ -465,6 +504,20 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **apps.StatefulSet: + require.NotNil(t, v) + + vl := *v + vn, err := k8s.AppsV1().StatefulSets(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) @@ -616,7 +669,7 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS *v = vn } default: - require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) + require.Fail(t, fmt.Sprintf("Unable to get object: %s", reflect.TypeOf(v).String())) } } } @@ -649,12 +702,24 @@ func SetMetaBasedOnType(t *testing.T, object meta.Object) { v.SetSelfLink(fmt.Sprintf("/api/v1/secrets/%s/%s", object.GetNamespace(), object.GetName())) + case *core.Service: + v.Kind = "Service" + v.APIVersion = "v1" + v.SetSelfLink(fmt.Sprintf("/api/v1/services/%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 *apps.StatefulSet: + v.Kind = "StatefulSet" + v.APIVersion = "v1" + v.SetSelfLink(fmt.Sprintf("/api/apps/v1/statefulsets/%s/%s", + object.GetNamespace(), + object.GetName())) case *api.ArangoDeployment: v.Kind = deployment.ArangoDeploymentResourceKind v.APIVersion = api.SchemeGroupVersion.String() @@ -790,10 +855,18 @@ func NewItem(t *testing.T, o operation.Operation, object meta.Object) operation. item.Group = "" item.Version = "v1" item.Kind = "Secret" + case *core.Service: + item.Group = "" + item.Version = "v1" + item.Kind = "Service" case *core.ServiceAccount: item.Group = "" item.Version = "v1" item.Kind = "ServiceAccount" + case *apps.StatefulSet: + item.Group = "apps" + item.Version = "v1" + item.Kind = "StatefulSet" case *api.ArangoDeployment: item.Group = deployment.ArangoDeploymentGroupName item.Version = api.ArangoDeploymentVersion diff --git a/pkg/util/tests/kubernetes_test.go b/pkg/util/tests/kubernetes_test.go index d9433c177..00af39dba 100644 --- a/pkg/util/tests/kubernetes_test.go +++ b/pkg/util/tests/kubernetes_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/stretchr/testify/require" + apps "k8s.io/api/apps/v1" batch "k8s.io/api/batch/v1" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" @@ -64,6 +65,8 @@ func Test_NewMetaObject(t *testing.T) { NewMetaObjectRun[*core.Pod](t) NewMetaObjectRun[*core.Secret](t) NewMetaObjectRun[*core.ServiceAccount](t) + NewMetaObjectRun[*core.Service](t) + NewMetaObjectRun[*apps.StatefulSet](t) NewMetaObjectRun[*rbac.Role](t) NewMetaObjectRun[*rbac.RoleBinding](t) NewMetaObjectRun[*rbac.ClusterRole](t)