diff --git a/CHANGELOG.md b/CHANGELOG.md index 0666ed2d3..15cbcddce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - (Feature) Discover Namespace in DebugPackage from K8S - (Feature) Expose Force CRD Install option - (Maintenance) Move Container utils functions +- (Feature) ArangoProfile Selectors ## [1.2.39](https://github.com/arangodb/kube-arangodb/tree/1.2.39) (2024-03-11) - (Feature) Extract Scheduler API diff --git a/docs/api/ArangoMLExtension.V1Alpha1.md b/docs/api/ArangoMLExtension.V1Alpha1.md index 7e122cd82..6dda8b824 100644 --- a/docs/api/ArangoMLExtension.V1Alpha1.md +++ b/docs/api/ArangoMLExtension.V1Alpha1.md @@ -71,6 +71,14 @@ Default Value: `false` *** +### .spec.deployment.imagePullSecrets + +Type: `array` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/pod/resources/image.go#L36) + +ImagePullSecrets define Secrets used to pull Image from registry + +*** + ### .spec.deployment.labels Type: `object` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go#L39) diff --git a/docs/api/ArangoProfile.V1Alpha1.md b/docs/api/ArangoProfile.V1Alpha1.md index 40746264c..3fa5b2541 100644 --- a/docs/api/ArangoProfile.V1Alpha1.md +++ b/docs/api/ArangoProfile.V1Alpha1.md @@ -8,6 +8,14 @@ title: ArangoProfile V1Alpha1 ## Spec +### .spec.selectors.label + +Type: `meta.LabelSelector` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/profile_selectors.go#L32) + +Label keeps information about label selector + +*** + ### .spec.template.container.all.env Type: `core.EnvVar` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/container/resources/environments.go#L36) @@ -281,6 +289,14 @@ Default Value: `false` *** +### .spec.template.pod.imagePullSecrets + +Type: `array` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/pod/resources/image.go#L36) + +ImagePullSecrets define Secrets used to pull Image from registry + +*** + ### .spec.template.pod.labels Type: `object` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.39/pkg/apis/scheduler/v1alpha1/pod/resources/metadata.go#L39) diff --git a/pkg/apis/scheduler/v1alpha1/container/resources/resources.go b/pkg/apis/scheduler/v1alpha1/container/resources/resources.go index bbe91f2a5..32beffc12 100644 --- a/pkg/apis/scheduler/v1alpha1/container/resources/resources.go +++ b/pkg/apis/scheduler/v1alpha1/container/resources/resources.go @@ -42,7 +42,10 @@ func (r *Resources) Apply(_ *core.PodTemplateSpec, template *core.Container) err return nil } - template.Resources = r.GetResources() + res := r.GetResources() + + template.Resources.Limits = kresources.UpscaleContainerResourceList(template.Resources.Limits, res.Limits) + template.Resources.Requests = kresources.UpscaleContainerResourceList(template.Resources.Requests, res.Requests) return nil } diff --git a/pkg/apis/scheduler/v1alpha1/container/resources/volume_mounts.go b/pkg/apis/scheduler/v1alpha1/container/resources/volume_mounts.go index fdbf5cd83..d3a5acb78 100644 --- a/pkg/apis/scheduler/v1alpha1/container/resources/volume_mounts.go +++ b/pkg/apis/scheduler/v1alpha1/container/resources/volume_mounts.go @@ -42,7 +42,7 @@ func (v *VolumeMounts) Apply(_ *core.PodTemplateSpec, container *core.Container) obj := v.DeepCopy() - container.VolumeMounts = obj.VolumeMounts + container.VolumeMounts = kresources.MergeVolumeMounts(container.VolumeMounts, obj.VolumeMounts...) return nil } diff --git a/pkg/apis/scheduler/v1alpha1/pod/definition.go b/pkg/apis/scheduler/v1alpha1/pod/definition.go index faf1448da..9648d2cfb 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/definition.go +++ b/pkg/apis/scheduler/v1alpha1/pod/definition.go @@ -34,6 +34,9 @@ type Pod struct { // Metadata keeps the metadata settings for Pod *schedulerPodResourcesApi.Metadata `json:",inline"` + // Image keeps the image information + *schedulerPodResourcesApi.Image `json:",inline"` + // Scheduling keeps the scheduling information *schedulerPodResourcesApi.Scheduling `json:",inline"` @@ -65,6 +68,7 @@ func (a *Pod) With(other *Pod) *Pod { return &Pod{ Scheduling: a.Scheduling.With(other.Scheduling), + Image: a.Image.With(other.Image), Namespace: a.Namespace.With(other.Namespace), Security: a.Security.With(other.Security), Volumes: a.Volumes.With(other.Volumes), @@ -80,6 +84,7 @@ func (a *Pod) Apply(template *core.PodTemplateSpec) error { return shared.WithErrors( a.Scheduling.Apply(template), + a.Image.Apply(template), a.Namespace.Apply(template), a.Security.Apply(template), a.Volumes.Apply(template), @@ -96,6 +101,14 @@ func (a *Pod) GetSecurity() *schedulerPodResourcesApi.Security { return a.Security } +func (a *Pod) GetImage() *schedulerPodResourcesApi.Image { + if a == nil { + return nil + } + + return a.Image +} + func (a *Pod) GetScheduling() *schedulerPodResourcesApi.Scheduling { if a == nil { return nil @@ -142,6 +155,7 @@ func (a *Pod) Validate() error { } return shared.WithErrors( a.Scheduling.Validate(), + a.Image.Validate(), a.Namespace.Validate(), a.Security.Validate(), a.Volumes.Validate(), diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/image.go b/pkg/apis/scheduler/v1alpha1/pod/resources/image.go new file mode 100644 index 000000000..495fe3e30 --- /dev/null +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/image.go @@ -0,0 +1,87 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 resources + +import ( + core "k8s.io/api/core/v1" + + "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/interfaces" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" +) + +type ImagePullSecrets []string + +var _ interfaces.Pod[Image] = &Image{} + +type Image struct { + // ImagePullSecrets define Secrets used to pull Image from registry + ImagePullSecrets ImagePullSecrets `json:"imagePullSecrets,omitempty"` +} + +func (i *Image) Apply(pod *core.PodTemplateSpec) error { + if i == nil { + return nil + } + + for _, secret := range i.ImagePullSecrets { + if hasImagePullSecret(pod.Spec.ImagePullSecrets, secret) { + continue + } + + pod.Spec.ImagePullSecrets = append(pod.Spec.ImagePullSecrets, core.LocalObjectReference{ + Name: secret, + }) + } + + return nil +} + +func (i *Image) With(other *Image) *Image { + if i == nil && other == nil { + return nil + } + + if other == nil { + return i.DeepCopy() + } + + return other.DeepCopy() +} + +func (i *Image) Validate() error { + if i == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceErrors("pullSecrets", shared.ValidateList(i.ImagePullSecrets, shared.ValidateResourceName)), + ) +} + +func hasImagePullSecret(secrets []core.LocalObjectReference, secret string) bool { + for _, sec := range secrets { + if sec.Name == secret { + return true + } + } + + return false +} diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/image_test.go b/pkg/apis/scheduler/v1alpha1/pod/resources/image_test.go new file mode 100644 index 000000000..0c44770b2 --- /dev/null +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/image_test.go @@ -0,0 +1,88 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 resources + +import ( + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" +) + +func applyImage(t *testing.T, template *core.PodTemplateSpec, ns ...*Image) func(in func(t *testing.T, pod *core.PodTemplateSpec)) { + var i *Image + + for _, n := range ns { + require.NoError(t, n.Validate()) + + i = i.With(n) + + require.NoError(t, i.Validate()) + } + + template = template.DeepCopy() + + if template == nil { + template = &core.PodTemplateSpec{} + } + + require.NoError(t, i.Apply(template)) + + return func(in func(t *testing.T, spec *core.PodTemplateSpec)) { + t.Run("Validate", func(t *testing.T) { + in(t, template) + }) + } +} + +func Test_Image(t *testing.T) { + t.Run("With Nil", func(t *testing.T) { + applyImage(t, nil, nil)(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Spec.ImagePullSecrets, 0) + }) + }) + t.Run("With Empty", func(t *testing.T) { + applyImage(t, &core.PodTemplateSpec{})(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Spec.ImagePullSecrets, 0) + }) + }) + t.Run("With PS", func(t *testing.T) { + applyImage(t, &core.PodTemplateSpec{}, &Image{ + ImagePullSecrets: []string{ + "secret", + }, + })(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Spec.ImagePullSecrets, 1) + require.Equal(t, "secret", pod.Spec.ImagePullSecrets[0].Name) + }) + }) + t.Run("With PS2", func(t *testing.T) { + applyImage(t, &core.PodTemplateSpec{}, &Image{ + ImagePullSecrets: []string{ + "secret", + "secret", + }, + })(func(t *testing.T, pod *core.PodTemplateSpec) { + require.Len(t, pod.Spec.ImagePullSecrets, 1) + require.Equal(t, "secret", pod.Spec.ImagePullSecrets[0].Name) + }) + }) +} diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/service_account.go b/pkg/apis/scheduler/v1alpha1/pod/resources/service_account.go index c8cea0b22..68099a796 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/resources/service_account.go +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/service_account.go @@ -46,7 +46,9 @@ func (s *ServiceAccount) Apply(template *core.PodTemplateSpec) error { c := s.DeepCopy() template.Spec.ServiceAccountName = c.ServiceAccountName - template.Spec.AutomountServiceAccountToken = c.AutomountServiceAccountToken + if c.AutomountServiceAccountToken != nil { + template.Spec.AutomountServiceAccountToken = c.AutomountServiceAccountToken + } return nil } diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/volumes.go b/pkg/apis/scheduler/v1alpha1/pod/resources/volumes.go index 3e561cdab..9e2bd6efd 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/resources/volumes.go +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/volumes.go @@ -43,7 +43,7 @@ func (v *Volumes) Apply(template *core.PodTemplateSpec) error { obj := v.DeepCopy() - template.Spec.Volumes = obj.Volumes + template.Spec.Volumes = kresources.MergeVolumes(template.Spec.Volumes, obj.Volumes...) return nil } diff --git a/pkg/apis/scheduler/v1alpha1/pod/resources/zz_generated.deepcopy.go b/pkg/apis/scheduler/v1alpha1/pod/resources/zz_generated.deepcopy.go index 7e833e994..b35c27c24 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/resources/zz_generated.deepcopy.go +++ b/pkg/apis/scheduler/v1alpha1/pod/resources/zz_generated.deepcopy.go @@ -30,6 +30,47 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Image) DeepCopyInto(out *Image) { + *out = *in + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make(ImagePullSecrets, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Image. +func (in *Image) DeepCopy() *Image { + if in == nil { + return nil + } + out := new(Image) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ImagePullSecrets) DeepCopyInto(out *ImagePullSecrets) { + { + in := &in + *out = make(ImagePullSecrets, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecrets. +func (in ImagePullSecrets) DeepCopy() ImagePullSecrets { + if in == nil { + return nil + } + out := new(ImagePullSecrets) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metadata) DeepCopyInto(out *Metadata) { *out = *in diff --git a/pkg/apis/scheduler/v1alpha1/pod/zz_generated.deepcopy.go b/pkg/apis/scheduler/v1alpha1/pod/zz_generated.deepcopy.go index 49c6f6a90..5274a706e 100644 --- a/pkg/apis/scheduler/v1alpha1/pod/zz_generated.deepcopy.go +++ b/pkg/apis/scheduler/v1alpha1/pod/zz_generated.deepcopy.go @@ -37,6 +37,11 @@ func (in *Pod) DeepCopyInto(out *Pod) { *out = new(resources.Metadata) (*in).DeepCopyInto(*out) } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(resources.Image) + (*in).DeepCopyInto(*out) + } if in.Scheduling != nil { in, out := &in.Scheduling, &out.Scheduling *out = new(resources.Scheduling) diff --git a/pkg/apis/scheduler/v1alpha1/profile_selectors.go b/pkg/apis/scheduler/v1alpha1/profile_selectors.go new file mode 100644 index 000000000..8e2f33470 --- /dev/null +++ b/pkg/apis/scheduler/v1alpha1/profile_selectors.go @@ -0,0 +1,49 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 v1alpha1 + +import ( + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + kresources "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/resources" +) + +type ProfileSelectors struct { + // Label keeps information about label selector + // +doc/type: meta.LabelSelector + Label *meta.LabelSelector `json:"label,omitempty"` +} + +func (p *ProfileSelectors) Validate() error { + if p == nil { + return nil + } + + return nil +} + +func (p *ProfileSelectors) Select(labels map[string]string) bool { + if p == nil || p.Label == nil { + return false + } + + return kresources.SelectLabels(p.Label, labels) +} diff --git a/pkg/apis/scheduler/v1alpha1/profile_spec.go b/pkg/apis/scheduler/v1alpha1/profile_spec.go index e1aa0f517..30bef5c4b 100644 --- a/pkg/apis/scheduler/v1alpha1/profile_spec.go +++ b/pkg/apis/scheduler/v1alpha1/profile_spec.go @@ -20,7 +20,25 @@ package v1alpha1 +import ( + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" +) + type ProfileSpec struct { + // Selectors keeps information about ProfileSelectors + Selectors *ProfileSelectors `json:"selectors,omitempty"` + // Template keeps the Profile Template Template *ProfileTemplate `json:"template,omitempty"` } + +func (p *ProfileSpec) Validate() error { + if p == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceErrors("selectors", p.Selectors.Validate()), + shared.PrefixResourceErrors("template", p.Template.Validate()), + ) +} diff --git a/pkg/apis/scheduler/v1alpha1/profile_templates.go b/pkg/apis/scheduler/v1alpha1/profile_templates.go index 34f984c71..f3e87557e 100644 --- a/pkg/apis/scheduler/v1alpha1/profile_templates.go +++ b/pkg/apis/scheduler/v1alpha1/profile_templates.go @@ -33,7 +33,7 @@ type ProfileTemplates []*ProfileTemplate func (p ProfileTemplates) Sort() ProfileTemplates { sort.Slice(p, func(i, j int) bool { if a, b := p[i].GetPriority(), p[j].GetPriority(); a != b { - return a < b + return a > b } return false @@ -45,8 +45,8 @@ func (p ProfileTemplates) Sort() ProfileTemplates { func (p ProfileTemplates) Merge() *ProfileTemplate { var z *ProfileTemplate - for _, q := range p { - z = z.With(q) + for id := len(p) - 1; id >= 0; id-- { + z = z.With(p[id]) } return z diff --git a/pkg/apis/scheduler/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/scheduler/v1alpha1/zz_generated.deepcopy.go index 0ebd19fd9..19ef99b6f 100644 --- a/pkg/apis/scheduler/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/scheduler/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,7 @@ package v1alpha1 import ( container "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/container" pod "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/pod" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -120,9 +121,35 @@ func (in *ProfileContainerTemplate) DeepCopy() *ProfileContainerTemplate { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProfileSelectors) DeepCopyInto(out *ProfileSelectors) { + *out = *in + if in.Label != nil { + in, out := &in.Label, &out.Label + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProfileSelectors. +func (in *ProfileSelectors) DeepCopy() *ProfileSelectors { + if in == nil { + return nil + } + out := new(ProfileSelectors) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProfileSpec) DeepCopyInto(out *ProfileSpec) { *out = *in + if in.Selectors != nil { + in, out := &in.Selectors, &out.Selectors + *out = new(ProfileSelectors) + (*in).DeepCopyInto(*out) + } if in.Template != nil { in, out := &in.Template, &out.Template *out = new(ProfileTemplate) diff --git a/pkg/crd/crds/ml-extension.schema.generated.yaml b/pkg/crd/crds/ml-extension.schema.generated.yaml index 2dbfcb115..ca6fbae51 100644 --- a/pkg/crd/crds/ml-extension.schema.generated.yaml +++ b/pkg/crd/crds/ml-extension.schema.generated.yaml @@ -349,6 +349,10 @@ v1alpha1: type: boolean hostPID: type: boolean + imagePullSecrets: + items: + type: string + type: array labels: additionalProperties: type: string diff --git a/pkg/crd/crds/scheduler-profile.schema.generated.yaml b/pkg/crd/crds/scheduler-profile.schema.generated.yaml index a104c3b1c..0b5b01b49 100644 --- a/pkg/crd/crds/scheduler-profile.schema.generated.yaml +++ b/pkg/crd/crds/scheduler-profile.schema.generated.yaml @@ -3,6 +3,31 @@ v1alpha1: properties: spec: properties: + selectors: + description: Selectors keeps information about ProfileSelectors + properties: + label: + description: Label keeps information about label selector + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + type: object template: description: Template keeps the Profile Template properties: @@ -930,6 +955,10 @@ v1alpha1: type: boolean hostPID: type: boolean + imagePullSecrets: + items: + type: string + type: array labels: additionalProperties: type: string diff --git a/pkg/util/k8sutil/resources/selectors.go b/pkg/util/k8sutil/resources/selectors.go new file mode 100644 index 000000000..8f2532fe0 --- /dev/null +++ b/pkg/util/k8sutil/resources/selectors.go @@ -0,0 +1,76 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 resources + +import ( + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/arangodb/kube-arangodb/pkg/util/strings" +) + +func SelectLabels(selector *meta.LabelSelector, labels map[string]string) bool { + if selector == nil { + return false + } + + for k, v := range selector.MatchLabels { + if v2, ok := labels[k]; !ok || v2 != v { + return false + } + } + + for _, req := range selector.MatchExpressions { + switch req.Operator { + case meta.LabelSelectorOpIn: + if len(req.Values) == 0 { + return false + } + + if v, ok := labels[req.Key]; !ok { + return false + } else if !strings.ListContains(req.Values, v) { + return false + } + case meta.LabelSelectorOpNotIn: + if len(req.Values) == 0 { + return false + } + + if v, ok := labels[req.Key]; ok { + if strings.ListContains(req.Values, v) { + return false + } + } + case meta.LabelSelectorOpExists: + if _, ok := labels[req.Key]; !ok { + return false + } + case meta.LabelSelectorOpDoesNotExist: + if _, ok := labels[req.Key]; ok { + return false + } + default: + return false + } + } + + return true +} diff --git a/pkg/util/k8sutil/resources/selectors_test.go b/pkg/util/k8sutil/resources/selectors_test.go new file mode 100644 index 000000000..3505d6006 --- /dev/null +++ b/pkg/util/k8sutil/resources/selectors_test.go @@ -0,0 +1,249 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 resources + +import ( + "testing" + + "github.com/stretchr/testify/require" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_Selectors_Labels(t *testing.T) { + labels := map[string]string{ + "A": "B", + "C": "D", + "E": "F", + } + + t.Run("Match", func(t *testing.T) { + t.Run("Do not match with nil", func(t *testing.T) { + require.False(t, SelectLabels(nil, nil)) + }) + t.Run("Match with any", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{}, nil)) + }) + t.Run("Match with dedicated labels select", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchLabels: map[string]string{ + "A": "B", + }, + }, labels)) + }) + t.Run("Match with multiple dedicated labels select", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchLabels: map[string]string{ + "A": "B", + "E": "F", + }, + }, labels)) + }) + t.Run("Match with mismatch dedicated labels select", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchLabels: map[string]string{ + "A": "B", + "E": "G", + }, + }, labels)) + }) + }) + + t.Run("Match Expression", func(t *testing.T) { + t.Run("Exists", func(t *testing.T) { + t.Run("Present", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, labels)) + }) + t.Run("Missing", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "B", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, labels)) + }) + }) + + t.Run("Exists", func(t *testing.T) { + t.Run("Present", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpDoesNotExist, + }, + }, + }, labels)) + }) + t.Run("Missing", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "B", + Operator: meta.LabelSelectorOpDoesNotExist, + }, + }, + }, labels)) + }) + }) + + t.Run("In", func(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpIn, + }, + }, + }, labels)) + }) + t.Run("Present", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpIn, + Values: []string{ + "B", + }, + }, + }, + }, labels)) + }) + t.Run("Present Multiple", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpIn, + Values: []string{ + "E", + "Z", + "B", + }, + }, + }, + }, labels)) + }) + t.Run("Missing", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "B", + Operator: meta.LabelSelectorOpIn, + Values: []string{ + "B", + }, + }, + }, + }, labels)) + }) + }) + + t.Run("NotIn", func(t *testing.T) { + t.Run("Not Existing", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "Z", + Operator: meta.LabelSelectorOpNotIn, + }, + }, + }, labels)) + }) + t.Run("Empty", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpNotIn, + }, + }, + }, labels)) + }) + t.Run("Present", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpNotIn, + Values: []string{ + "B", + }, + }, + }, + }, labels)) + }) + t.Run("Present Multiple", func(t *testing.T) { + require.False(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpNotIn, + Values: []string{ + "E", + "Z", + "B", + }, + }, + }, + }, labels)) + }) + t.Run("Missing", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "B", + Operator: meta.LabelSelectorOpNotIn, + Values: []string{ + "B", + }, + }, + }, + }, labels)) + }) + t.Run("Missing Value", func(t *testing.T) { + require.True(t, SelectLabels(&meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "A", + Operator: meta.LabelSelectorOpNotIn, + Values: []string{ + "R", + "Z", + "D", + }, + }, + }, + }, labels)) + }) + }) + }) +} diff --git a/pkg/util/tests/kubernetes.go b/pkg/util/tests/kubernetes.go index 7e0dc23e4..eea972e4e 100644 --- a/pkg/util/tests/kubernetes.go +++ b/pkg/util/tests/kubernetes.go @@ -41,6 +41,8 @@ import ( api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/apis/ml" mlApi "github.com/arangodb/kube-arangodb/pkg/apis/ml/v1alpha1" + "github.com/arangodb/kube-arangodb/pkg/apis/scheduler" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1" arangoClientSet "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" operator "github.com/arangodb/kube-arangodb/pkg/operatorV2" "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" @@ -205,6 +207,12 @@ func CreateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := arango.MlV1alpha1().ArangoMLCronJobs(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **schedulerApi.ArangoProfile: + require.NotNil(t, v) + + vl := *v + _, err := arango.SchedulerV1alpha1().ArangoProfiles(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())) } @@ -325,6 +333,12 @@ func UpdateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.RbacV1().RoleBindings(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) require.NoError(t, err) + case **schedulerApi.ArangoProfile: + require.NotNil(t, v) + + vl := *v + _, err := arango.SchedulerV1alpha1().ArangoProfiles(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())) } @@ -700,6 +714,21 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **schedulerApi.ArangoProfile: + require.NotNil(t, v) + + vl := *v + + vn, err := arango.SchedulerV1alpha1().ArangoProfiles(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 get object: %s", reflect.TypeOf(v).String())) } @@ -832,11 +861,23 @@ func SetMetaBasedOnType(t *testing.T, object meta.Object) { ml.ArangoMLCronJobResourcePlural, object.GetNamespace(), object.GetName())) + case *schedulerApi.ArangoProfile: + v.Kind = scheduler.ArangoProfileResourceKind + v.APIVersion = schedulerApi.SchemeGroupVersion.String() + v.SetSelfLink(fmt.Sprintf("/api/%s/%s/%s/%s", + schedulerApi.SchemeGroupVersion.String(), + scheduler.ArangoProfileResourcePlural, + object.GetNamespace(), + object.GetName())) default: require.Fail(t, fmt.Sprintf("Unable to create object: %s", reflect.TypeOf(v).String())) } } +func NewMetaObjectInDefaultNamespace[T meta.Object](t *testing.T, name string, mods ...MetaObjectMod[T]) T { + return NewMetaObject[T](t, FakeNamespace, name, mods...) +} + func NewMetaObject[T meta.Object](t *testing.T, namespace, name string, mods ...MetaObjectMod[T]) T { var obj T @@ -951,6 +992,10 @@ func NewItem(t *testing.T, o operation.Operation, object meta.Object) operation. item.Group = ml.ArangoMLGroupName item.Version = mlApi.ArangoMLVersion item.Kind = ml.ArangoMLCronJobResourceKind + case *schedulerApi.ArangoProfile: + item.Group = scheduler.ArangoSchedulerGroupName + item.Version = schedulerApi.ArangoSchedulerVersion + item.Kind = scheduler.ArangoProfileResourceKind 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 00af39dba..421373982 100644 --- a/pkg/util/tests/kubernetes_test.go +++ b/pkg/util/tests/kubernetes_test.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2023-2024 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. @@ -34,6 +34,7 @@ import ( backupApi "github.com/arangodb/kube-arangodb/pkg/apis/backup/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" mlApi "github.com/arangodb/kube-arangodb/pkg/apis/ml/v1alpha1" + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1" "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" "github.com/arangodb/kube-arangodb/pkg/util/kclient" ) @@ -76,4 +77,5 @@ func Test_NewMetaObject(t *testing.T) { NewMetaObjectRun[*backupApi.ArangoBackup](t) NewMetaObjectRun[*mlApi.ArangoMLExtension](t) NewMetaObjectRun[*mlApi.ArangoMLStorage](t) + NewMetaObjectRun[*schedulerApi.ArangoProfile](t) } diff --git a/pkg/util/tests/resources.go b/pkg/util/tests/resources.go new file mode 100644 index 000000000..5241e4971 --- /dev/null +++ b/pkg/util/tests/resources.go @@ -0,0 +1,36 @@ +// +// DISCLAIMER +// +// Copyright 2024 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 tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + + kresources "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/resources" +) + +func GetContainerByNameT(t *testing.T, containers []core.Container, name string) core.Container { + id := kresources.GetContainerIDByName(containers, name) + require.NotEqualValues(t, -1, id) + return containers[id] +}