diff --git a/cmd/cmd.go b/cmd/cmd.go index bf85f80dd..116c7108f 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -75,7 +75,6 @@ const ( defaultServerPort = 8528 defaultAPIHTTPPort = 8628 defaultAPIGRPCPort = 8728 - defaultLogLevel = "info" defaultAdminSecretName = "arangodb-operator-dashboard" defaultAPIJWTSecretName = "arangodb-operator-api-jwt" defaultAPIJWTKeySecretName = "arangodb-operator-api-jwt-key" @@ -96,9 +95,6 @@ var ( hardLimit uint64 } - logFormat string - logLevels []string - logSampling bool serverOptions struct { host string port int @@ -195,9 +191,6 @@ func init() { f.StringVar(&serverOptions.tlsSecretName, "server.tls-secret-name", "", "Name of secret containing tls.crt & tls.key for HTTPS server (if empty, self-signed certificate is used)") f.StringVar(&serverOptions.adminSecretName, "server.admin-secret-name", defaultAdminSecretName, "Name of secret containing username + password for login to the dashboard") f.BoolVar(&serverOptions.allowAnonymous, "server.allow-anonymous-access", false, "Allow anonymous access to the dashboard") - f.StringVar(&logFormat, "log.format", "pretty", "Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used") - f.StringArrayVar(&logLevels, "log.level", []string{defaultLogLevel}, fmt.Sprintf("Set log levels in format or =. Possible loggers: %s", strings.Join(logging.Global().Names(), ", "))) - f.BoolVar(&logSampling, "log.sampling", true, "If true, operator will try to minimize duplication of logging events") f.BoolVar(&apiOptions.enabled, "api.enabled", true, "Enable operator HTTP and gRPC API") f.IntVar(&apiOptions.httpPort, "api.http-port", defaultAPIHTTPPort, "HTTP API port to listen on") f.IntVar(&apiOptions.grpcPort, "api.grpc-port", defaultAPIGRPCPort, "gRPC API port to listen on") @@ -247,6 +240,9 @@ func init() { f.StringArrayVar(&metricsOptions.excludedMetricPrefixes, "metrics.excluded-prefixes", nil, "List of the excluded metrics prefixes") f.BoolVar(&operatorImageDiscovery.defaultStatusDiscovery, "image.discovery.status", true, "Discover Operator Image from Pod Status by default. When disabled Pod Spec is used.") f.DurationVar(&operatorImageDiscovery.timeout, "image.discovery.timeout", time.Minute, "Timeout for image discovery process") + if err := logging.Init(&cmdMain); err != nil { + panic(err.Error()) + } if err := features.Init(&cmdMain); err != nil { panic(err.Error()) } @@ -308,24 +304,10 @@ func executeMain(cmd *cobra.Command, args []string) { kclient.SetDefaultBurst(operatorKubernetesOptions.burst) // Prepare log service - var err error - - levels, err := logging.ParseLogLevelsFromArgs(logLevels) - if err != nil { - logger.Err(err).Fatal("Unable to parse log level") + if err := logging.Enable(); err != nil { + logger.Err(err).Fatal("Unable to enable logger") } - // Set root logger to stdout (JSON formatted) if not prettified - if strings.ToUpper(logFormat) == "JSON" { - logging.Global().SetRoot(zerolog.New(os.Stdout).With().Timestamp().Logger()) - } else if strings.ToLower(logFormat) != "pretty" && logFormat != "" { - logger.Fatal("Unknown log format: %s", logFormat) - } - logging.Global().Configure(logging.Config{ - Levels: levels, - Sampling: logSampling, - }) - podNameParts := strings.Split(name, "-") operatorID := podNameParts[len(podNameParts)-1] @@ -347,16 +329,16 @@ func executeMain(cmd *cobra.Command, args []string) { !operatorOptions.enableBackup && !operatorOptions.enableApps && !operatorOptions.enableK2KClusterSync && !operatorOptions.enableML { if !operatorOptions.versionOnly { if version.GetVersionV1().IsEnterprise() { - logger.Err(err).Fatal("Turn on --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync, --operator.ml or any combination of these") + logger.Fatal("Turn on --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync, --operator.ml or any combination of these") } else { - logger.Err(err).Fatal("Turn on --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync or any combination of these") + logger.Fatal("Turn on --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync or any combination of these") } } } else if operatorOptions.versionOnly { - logger.Err(err).Fatal("Options --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync, --operator.ml cannot be enabled together with --operator.version") + logger.Fatal("Options --operator.deployment, --operator.deployment-replication, --operator.storage, --operator.backup, --operator.apps, --operator.k2k-cluster-sync, --operator.ml cannot be enabled together with --operator.version") } else if !version.GetVersionV1().IsEnterprise() { if operatorOptions.enableML { - logger.Err(err).Fatal("Options --operator.ml can be enabled only on the Enterprise Operator") + logger.Fatal("Options --operator.ml can be enabled only on the Enterprise Operator") } } @@ -444,7 +426,11 @@ func executeMain(cmd *cobra.Command, args []string) { if err != nil { logger.Err(err).Fatal("Failed to create API server") } - go errors.LogError(logger, "while running API server", apiServer.Run) + go func() { + if err := apiServer.Run(); err != nil { + logger.Err(err).Error("while running API server") + } + }() } listenAddr := net.JoinHostPort(serverOptions.host, strconv.Itoa(serverOptions.port)) @@ -493,7 +479,11 @@ func executeMain(cmd *cobra.Command, args []string) { }); err != nil { logger.Err(err).Fatal("Failed to create HTTP server") } else { - go errors.LogError(logger, "error while starting server", svr.Run) + go func() { + if err := svr.Run(); err != nil { + logger.Err(err).Error("error while starting server") + } + }() } // startChaos(context.Background(), cfg.KubeCli, cfg.Namespace, chaosLevel) diff --git a/cmd/scheduler.go b/cmd/scheduler.go new file mode 100644 index 000000000..32fd48cc8 --- /dev/null +++ b/cmd/scheduler.go @@ -0,0 +1,39 @@ +// +// 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 cmd + +import ( + "github.com/spf13/cobra" + + "github.com/arangodb/kube-arangodb/pkg/scheduler" +) + +func init() { + cmd := &cobra.Command{ + Use: "scheduler", + } + + if err := scheduler.InitCommand(cmd); err != nil { + panic(err.Error()) + } + + cmdMain.AddCommand(cmd) +} diff --git a/pkg/logging/cli.go b/pkg/logging/cli.go new file mode 100644 index 000000000..381561b9b --- /dev/null +++ b/pkg/logging/cli.go @@ -0,0 +1,85 @@ +// +// 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 logging + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/rs/zerolog" + "github.com/spf13/cobra" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +const ( + defaultLogLevel = "info" +) + +var ( + enableLock sync.Mutex + enabled bool + + cli struct { + format string + levels []string + sampling bool + } +) + +func Init(cmd *cobra.Command) error { + f := cmd.PersistentFlags() + + f.StringVar(&cli.format, "log.format", "pretty", "Set log format. Allowed values: 'pretty', 'JSON'. If empty, default format is used") + f.StringArrayVar(&cli.levels, "log.level", []string{defaultLogLevel}, fmt.Sprintf("Set log levels in format or =. Possible loggers: %s", strings.Join(Global().Names(), ", "))) + f.BoolVar(&cli.sampling, "log.sampling", true, "If true, operator will try to minimize duplication of logging events") + + return nil +} + +func Enable() error { + enableLock.Lock() + defer enableLock.Unlock() + + if enabled { + return errors.Errorf("Logger already enabled") + } + + levels, err := ParseLogLevelsFromArgs(cli.levels) + if err != nil { + return errors.WithMessagef(err, "Unable to parse levels") + } + + // Set root logger to stdout (JSON formatted) if not prettified + if strings.ToUpper(cli.format) == "JSON" { + Global().SetRoot(zerolog.New(os.Stdout).With().Timestamp().Logger()) + } else if strings.ToLower(cli.format) != "pretty" && cli.format != "" { + return errors.Errorf("Unknown log format: %s", cli.format) + } + Global().Configure(Config{ + Levels: levels, + Sampling: cli.sampling, + }) + + return nil +} diff --git a/pkg/scheduler/cli.go b/pkg/scheduler/cli.go new file mode 100644 index 000000000..3a9c31a2f --- /dev/null +++ b/pkg/scheduler/cli.go @@ -0,0 +1,150 @@ +// +// 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 scheduler + +import ( + "context" + "os" + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/kclient" +) + +func InitCommand(cmd *cobra.Command) error { + var c cli + return c.register(cmd) +} + +type cli struct { + Namespace string + + Labels []string + Envs []string + + Profiles []string + + Container string + + Image string +} + +func (c *cli) asRequest(args ...string) (Request, error) { + var r = Request{ + Labels: map[string]string{}, + Envs: map[string]string{}, + } + + for _, l := range c.Labels { + p := strings.SplitN(l, "=", 2) + if len(p) == 1 { + r.Labels[p[0]] = "" + logger.Debug("Label Discovered: %s", p[0]) + } else { + r.Labels[p[0]] = p[1] + logger.Debug("Label Discovered: %s=%s", p[0], p[1]) + } + } + + for _, l := range c.Envs { + p := strings.SplitN(l, "=", 2) + if len(p) == 1 { + return r, errors.Errorf("Missing value for env: %s", p[0]) + } else { + r.Envs[p[0]] = p[1] + logger.Debug("Env Discovered: %s=%s", p[0], p[1]) + } + } + + if len(c.Profiles) > 0 { + r.Profiles = c.Profiles + logger.Debug("Enabling profiles: %s", strings.Join(c.Profiles, ", ")) + } + + r.Container = util.NewType(c.Container) + if c.Image != "" { + r.Image = util.NewType(c.Image) + } + + r.Args = args + + return r, nil +} + +func (c *cli) register(cmd *cobra.Command) error { + if err := logging.Init(cmd); err != nil { + return err + } + + cmd.RunE = c.run + + f := cmd.PersistentFlags() + + f.StringVarP(&c.Namespace, "namespace", "n", constants.NamespaceWithDefault("default"), "Kubernetes namespace") + f.StringSliceVarP(&c.Labels, "label", "l", nil, "Scheduler Render Labels in format =") + f.StringSliceVarP(&c.Envs, "env", "e", nil, "Scheduler Render Envs in format =") + f.StringSliceVarP(&c.Profiles, "profile", "p", nil, "Scheduler Render Profiles") + f.StringVar(&c.Container, "container", DefaultContainerName, "Container Name") + f.StringVar(&c.Image, "image", "", "Image") + + return nil +} + +func (c *cli) run(cmd *cobra.Command, args []string) error { + if err := logging.Enable(); err != nil { + return err + } + + r, err := c.asRequest() + if err != nil { + return err + } + + k, ok := kclient.GetDefaultFactory().Client() + if !ok { + return errors.Errorf("Unable to create Kubernetes Client") + } + + s := NewScheduler(k, c.Namespace) + + rendered, profiles, err := s.Render(context.Background(), r) + if err != nil { + return err + } + logger.Debug("Enabled profiles: %s", strings.Join(profiles, ", ")) + + data, err := yaml.Marshal(rendered) + if err != nil { + return err + } + + if _, err := util.WriteAll(os.Stdout, data); err != nil { + return err + } + + return nil +} diff --git a/pkg/scheduler/input.go b/pkg/scheduler/input.go new file mode 100644 index 000000000..03f3c885c --- /dev/null +++ b/pkg/scheduler/input.go @@ -0,0 +1,91 @@ +// +// 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 scheduler + +import ( + "math" + + core "k8s.io/api/core/v1" + + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1" + schedulerContainerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/container" + schedulerContainerResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/container/resources" + schedulerPodApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/pod" + schedulerPodResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/pod/resources" + "github.com/arangodb/kube-arangodb/pkg/util" +) + +const DefaultContainerName = "job" + +type Request struct { + Labels map[string]string + + Profiles []string + + Envs map[string]string + + Container *string + + Image *string + + Args []string +} + +func (r Request) AsTemplate() *schedulerApi.ProfileTemplate { + var container schedulerContainerApi.Container + + if len(r.Envs) > 0 { + container.Environments = &schedulerContainerResourcesApi.Environments{} + + for k, v := range r.Envs { + container.Environments.Env = append(container.Environments.Env, core.EnvVar{ + Name: k, + Value: v, + }) + } + } + + if len(r.Args) > 0 { + container.Core = &schedulerContainerResourcesApi.Core{ + Args: r.Args, + } + } + + if r.Image != nil { + container.Image = &schedulerContainerResourcesApi.Image{ + Image: util.NewType(util.TypeOrDefault(r.Image)), + } + } + + return &schedulerApi.ProfileTemplate{ + Priority: util.NewType(math.MaxInt), + Pod: &schedulerPodApi.Pod{ + Metadata: &schedulerPodResourcesApi.Metadata{ + Labels: util.MergeMaps(true, r.Labels), + }, + }, + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: map[string]schedulerContainerApi.Container{ + util.TypeOrDefault(r.Container, DefaultContainerName): container, + }, + }, + } +} diff --git a/pkg/scheduler/logger.go b/pkg/scheduler/logger.go new file mode 100644 index 000000000..b470e9a2a --- /dev/null +++ b/pkg/scheduler/logger.go @@ -0,0 +1,25 @@ +// +// 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 scheduler + +import "github.com/arangodb/kube-arangodb/pkg/logging" + +var logger = logging.Global().RegisterAndGetLogger("scheduler", logging.Info) diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go new file mode 100644 index 000000000..2f53804b8 --- /dev/null +++ b/pkg/scheduler/scheduler.go @@ -0,0 +1,119 @@ +// +// 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 scheduler + +import ( + "context" + + core "k8s.io/api/core/v1" + + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1" + "github.com/arangodb/kube-arangodb/pkg/debug_package/generators/kubernetes" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/kclient" +) + +func NewScheduler(client kclient.Client, namespace string) Scheduler { + return scheduler{ + client: client, + namespace: namespace, + } +} + +type Scheduler interface { + Render(ctx context.Context, in Request, templates ...*schedulerApi.ProfileTemplate) (*core.PodTemplateSpec, []string, error) +} + +type scheduler struct { + client kclient.Client + namespace string +} + +func (s scheduler) Render(ctx context.Context, in Request, templates ...*schedulerApi.ProfileTemplate) (*core.PodTemplateSpec, []string, error) { + profileMap, err := kubernetes.MapObjects[*schedulerApi.ArangoProfileList, *schedulerApi.ArangoProfile](ctx, s.client.Arango().SchedulerV1alpha1().ArangoProfiles(s.namespace), func(result *schedulerApi.ArangoProfileList) []*schedulerApi.ArangoProfile { + q := make([]*schedulerApi.ArangoProfile, len(result.Items)) + + for id, e := range result.Items { + q[id] = e.DeepCopy() + } + + return q + }) + + if err != nil { + return nil, nil, err + } + + profiles := profileMap.AsList().Filter(func(a *schedulerApi.ArangoProfile) bool { + return a != nil && a.Spec.Template != nil + }).Filter(func(a *schedulerApi.ArangoProfile) bool { + if a.Spec.Selectors == nil { + return false + } + + if !a.Spec.Selectors.Select(in.Labels) { + return false + } + + return true + }) + + for _, name := range in.Profiles { + p, ok := profileMap.ByName(name) + if !ok { + return nil, nil, errors.Errorf("Profile with name `%s` is missing", name) + } + + profiles = append(profiles, p) + } + + profiles = profiles.Unique(func(existing kubernetes.List[*schedulerApi.ArangoProfile], o *schedulerApi.ArangoProfile) bool { + return existing.Contains(func(a *schedulerApi.ArangoProfile) bool { + return a.GetName() == o.GetName() + }) + }) + + profiles = profiles.Sort(func(a, b *schedulerApi.ArangoProfile) bool { + return a.Spec.Template.GetPriority() > b.Spec.Template.GetPriority() + }) + + if err := errors.Errors(kubernetes.Extract(profiles, func(in *schedulerApi.ArangoProfile) error { + return in.Spec.Validate() + })...); err != nil { + return nil, nil, err + } + + extracted := schedulerApi.ProfileTemplates(kubernetes.Extract(profiles, func(in *schedulerApi.ArangoProfile) *schedulerApi.ProfileTemplate { + return in.Spec.Template + }).Append(templates...).Append(in.AsTemplate())) + + names := kubernetes.Extract(profiles, func(in *schedulerApi.ArangoProfile) string { + return in.GetName() + }) + + var pod core.PodTemplateSpec + + if err := extracted.RenderOnTemplate(&pod); err != nil { + return nil, names, err + } + + return &pod, names, nil +} diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go new file mode 100644 index 000000000..e41ba0254 --- /dev/null +++ b/pkg/scheduler/scheduler_test.go @@ -0,0 +1,289 @@ +// +// 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 scheduler + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + schedulerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1" + schedulerContainerApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/container" + schedulerContainerResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1alpha1/container/resources" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/kclient" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func newScheduler(t *testing.T, objects ...*schedulerApi.ArangoProfile) Scheduler { + client := kclient.NewFakeClientBuilder().Client() + + objs := make([]interface{}, len(objects)) + for id := range objs { + objs[id] = &objects[id] + } + + tests.CreateObjects(t, client.Kubernetes(), client.Arango(), objs...) + + return NewScheduler(client, tests.FakeNamespace) +} + +type validatorExec func(in validator) + +type validator func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) + +func render(t *testing.T, s Scheduler, in Request, templates ...*schedulerApi.ProfileTemplate) validatorExec { + pod, accepted, err := s.Render(context.Background(), in, templates...) + t.Logf("Accepted templates: %s", strings.Join(accepted, ", ")) + if err != nil { + return runValidate(t, err, pod, accepted) + } + require.NoError(t, err) + + data, err := yaml.Marshal(pod) + require.NoError(t, err) + + t.Logf("Rendered Template:\n%s", string(data)) + + return runValidate(t, nil, pod, accepted) +} + +func runValidate(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) validatorExec { + return func(in validator) { + t.Run("Validate", func(t *testing.T) { + in(t, err, template, accepted) + }) + } +} + +func Test_NoProfiles(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test")), Request{})(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 0) + + tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + }) +} + +func Test_MissingSelectedProfile(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test")), Request{ + Profiles: []string{"missing"}, + })(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.EqualError(t, err, "Profile with name `missing` is missing") + }) +} + +func Test_SelectorWithoutSelector(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:1"), + }, + }, + }, + }, + } + })), Request{})(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 0) + + c := tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + require.Equal(t, "", c.Image) + }) +} + +func Test_SelectorWithSelectorAll(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Selectors = &schedulerApi.ProfileSelectors{ + Label: &meta.LabelSelector{}, + } + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:1"), + }, + }, + }, + }, + } + })), Request{})(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 1) + require.Equal(t, []string{ + "test", + }, accepted) + + c := tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + require.Equal(t, "image:1", c.Image) + }) +} + +func Test_SelectorWithSpecificSelector_MissingLabel(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Selectors = &schedulerApi.ProfileSelectors{ + Label: &meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "ml.arangodb.com/type", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, + } + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:1"), + }, + }, + }, + }, + } + })), Request{})(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 0) + + c := tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + require.Equal(t, "", c.Image) + }) +} + +func Test_SelectorWithSpecificSelector_PresentLabel(t *testing.T) { + render(t, newScheduler(t, tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Selectors = &schedulerApi.ProfileSelectors{ + Label: &meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "ml.arangodb.com/type", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, + } + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:1"), + }, + }, + }, + }, + } + })), Request{ + Labels: map[string]string{ + "ml.arangodb.com/type": "training", + }, + }, nil)(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 1) + require.Equal(t, []string{ + "test", + }, accepted) + + c := tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + require.Equal(t, "image:1", c.Image) + }) +} + +func Test_SelectorWithSpecificSelector_PresentLabel_ByPriority(t *testing.T) { + render(t, newScheduler(t, + tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Selectors = &schedulerApi.ProfileSelectors{ + Label: &meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "ml.arangodb.com/type", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, + } + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Priority: util.NewType(1), + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:1"), + }, + }, + }, + }, + } + }), tests.NewMetaObjectInDefaultNamespace[*schedulerApi.ArangoProfile](t, "test2", func(t *testing.T, obj *schedulerApi.ArangoProfile) { + obj.Spec.Selectors = &schedulerApi.ProfileSelectors{ + Label: &meta.LabelSelector{ + MatchExpressions: []meta.LabelSelectorRequirement{ + { + Key: "ml.arangodb.com/type", + Operator: meta.LabelSelectorOpExists, + }, + }, + }, + } + obj.Spec.Template = &schedulerApi.ProfileTemplate{ + Priority: util.NewType(2), + Container: &schedulerApi.ProfileContainerTemplate{ + Containers: schedulerContainerApi.Containers{ + DefaultContainerName: { + Image: &schedulerContainerResourcesApi.Image{ + Image: util.NewType("image:2"), + }, + }, + }, + }, + } + })), Request{ + Labels: map[string]string{ + "ml.arangodb.com/type": "training", + }, + })(func(t *testing.T, err error, template *core.PodTemplateSpec, accepted []string) { + require.NoError(t, err) + + require.Len(t, accepted, 2) + require.Equal(t, []string{ + "test2", + "test", + }, accepted) + + c := tests.GetContainerByNameT(t, template.Spec.Containers, DefaultContainerName) + require.Equal(t, "image:2", c.Image) + }) +} diff --git a/pkg/util/errors/errors.go b/pkg/util/errors/errors.go index fd249da1c..221b8948a 100644 --- a/pkg/util/errors/errors.go +++ b/pkg/util/errors/errors.go @@ -31,8 +31,6 @@ import ( "github.com/pkg/errors" driver "github.com/arangodb/go-driver" - - "github.com/arangodb/kube-arangodb/pkg/logging" ) func Cause(err error) error { @@ -209,12 +207,6 @@ func libCause(err error) (bool, error) { } } -func LogError(logger logging.Logger, msg string, f func() error) { - if err := f(); err != nil { - logger.Err(err).Error(msg) - } -} - type Causer interface { Cause() error }