diff --git a/controllers/sustainability/deployment_resource.go b/controllers/sustainability/deployment_resource.go index eb4ad5d..971c8a6 100644 --- a/controllers/sustainability/deployment_resource.go +++ b/controllers/sustainability/deployment_resource.go @@ -13,6 +13,11 @@ import ( "github.com/kristofferahl/aeto/internal/pkg/reconcile" ) +const ( + DeploymentResourceApiVersion string = "apps/v1" + DeploymentResourceKind string = "Deployment" +) + type DeploymentResource struct { deployments []appsv1.Deployment replicas []DeploymentReplicas @@ -28,15 +33,7 @@ func NewDeploymentResource(c kubernetes.Client, rctx reconcile.Context, savingsp replicas: replicas, } - hasDeployments := false - for _, target := range savingspolicy.Spec.Targets { - if target.ApiVersion == "apps/v1" && target.Kind == "Deployment" && !target.Ignore { - hasDeployments = true - break - } - } - - if hasDeployments { + if hasDeploymentTargets(savingspolicy) { var deployments appsv1.DeploymentList if err := c.List(rctx, &deployments, &client.ListOptions{Namespace: rctx.Request.Namespace}); err != nil { return r, err @@ -45,18 +42,19 @@ func NewDeploymentResource(c kubernetes.Client, rctx reconcile.Context, savingsp filtered := make([]appsv1.Deployment, 0) for _, d := range deployments.Items { - ignore := false - for _, t := range savingspolicy.Spec.Targets { - if t.ApiVersion == "apps/v1" && t.Kind == "Deployment" && t.Name == d.Name && t.Ignore { - ignore = true - break + if ignoreDeploymentTarget(savingspolicy, d) { + replicas, found := r.originalReplicas(d.Name) + if found && replicas > 0 { + rctx.Log.V(1).Info("ignoring previously targeted deployment, trying wake up before ignoring", "deployment", d.Name, "previous-replicas", replicas) + err := r.wakeUpDeployment(c, rctx, d) + if err != nil { + rctx.Log.Error(err, "failed to wake up previously targeted deployment, will have to retry before ignoring", "deployment", d.Name, "previous-replicas", replicas) + return r, err + } } - } - - if !ignore { - filtered = append(filtered, d) - } else { rctx.Log.V(1).Info("ignoring deployment", "deployment", d.Name) + } else { + filtered = append(filtered, d) } } @@ -72,6 +70,7 @@ func (r DeploymentResource) HasResource() bool { func (r DeploymentResource) Sleep(c kubernetes.Client, rctx reconcile.Context) error { for _, d := range r.deployments { + rctx.Log.V(1).Info("ensuring deployment i scaled to 0", "deployment", d.Name) if *d.Spec.Replicas != 0 { if err := r.scaleTo(c.GetClient(), rctx, d, 0, *d.Spec.Replicas); err != nil { return err @@ -84,21 +83,9 @@ func (r DeploymentResource) Sleep(c kubernetes.Client, rctx reconcile.Context) e func (r DeploymentResource) WakeUp(c kubernetes.Client, rctx reconcile.Context) error { for _, d := range r.deployments { - if *d.Spec.Replicas != 0 { - rctx.Log.Info("deployment replicas not set to 0, skipping wake up", "deployment", d.Name) - continue - } - - replicas, ok := r.originalReplicas(d.Name) - if !ok { - rctx.Log.Info("deployment not tracked in state, unable to wake up", "deployment", d.Name) - continue - } - - if *d.Spec.Replicas != replicas { - if err := r.scaleTo(c.GetClient(), rctx, d, replicas, *d.Spec.Replicas); err != nil { - return err - } + err := r.wakeUpDeployment(c, rctx, d) + if err != nil { + return err } } @@ -125,6 +112,27 @@ func (r DeploymentResource) Info() ([]byte, error) { return json.Marshal(deploymentReplicas) } +func (r DeploymentResource) wakeUpDeployment(c kubernetes.Client, rctx reconcile.Context, d appsv1.Deployment) error { + if *d.Spec.Replicas != 0 { + rctx.Log.V(1).Info("deployment replicas not set to 0, skipping wake up", "deployment", d.Name) + return nil + } + + replicas, ok := r.originalReplicas(d.Name) + if !ok { + rctx.Log.Info("deployment not tracked in state, unable to wake up", "deployment", d.Name) + return nil + } + + if *d.Spec.Replicas != replicas { + if err := r.scaleTo(c.GetClient(), rctx, d, replicas, *d.Spec.Replicas); err != nil { + return err + } + } + + return nil +} + func (r DeploymentResource) originalReplicas(name string) (int32, bool) { for _, r := range r.replicas { if r.Name == name { @@ -156,3 +164,21 @@ func ConvertToDeploymentsInfo(data []byte) ([]DeploymentReplicas, error) { return deploymentReplicas, nil } + +func hasDeploymentTargets(savingspolicy sustainabilityv1alpha1.SavingsPolicy) bool { + for _, target := range savingspolicy.Spec.Targets { + if target.ApiVersion == DeploymentResourceApiVersion && target.Kind == DeploymentResourceKind { + return true + } + } + return false +} + +func ignoreDeploymentTarget(savingspolicy sustainabilityv1alpha1.SavingsPolicy, deployment appsv1.Deployment) bool { + for _, t := range savingspolicy.Spec.Targets { + if t.ApiVersion == DeploymentResourceApiVersion && t.Kind == DeploymentResourceKind && (t.Name == "" || t.Name == deployment.Name) && t.Ignore { + return true + } + } + return false +} diff --git a/controllers/sustainability/deployment_resource_test.go b/controllers/sustainability/deployment_resource_test.go new file mode 100644 index 0000000..0a5b17c --- /dev/null +++ b/controllers/sustainability/deployment_resource_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2022. + +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. +*/ + +package sustainability + +import ( + sustainabilityv1alpha1 "github.com/kristofferahl/aeto/apis/sustainability/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var _ = Describe("Deployment Resource", func() { + Describe("SavingsPolicy", func() { + Describe("has deployment targets?", func() { + Context("with deployment targets in policy", func() { + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: DeploymentResourceKind, + ApiVersion: DeploymentResourceApiVersion, + Ignore: true, + }, + }, + }, + } + result := hasDeploymentTargets(sp) + + It("should return true", func() { + Expect(result).To(BeTrue()) + }) + }) + + Context("with no deployment targets", func() { + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: "Foobar", + ApiVersion: DeploymentResourceApiVersion, + Ignore: true, + }, + }, + }, + } + result := hasDeploymentTargets(sp) + + It("should return false", func() { + Expect(result).To(BeFalse()) + }) + }) + }) + + Describe("ignore deployment target?", func() { + It("should return false when target matches all deployments", func() { + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: DeploymentResourceKind, + ApiVersion: DeploymentResourceApiVersion, + Ignore: false, + }, + }, + }, + } + + d := appsv1.Deployment{ + TypeMeta: v1.TypeMeta{ + Kind: DeploymentResourceKind, + APIVersion: DeploymentResourceApiVersion, + }, + ObjectMeta: v1.ObjectMeta{ + Name: "foobar", + }, + } + + result := ignoreDeploymentTarget(sp, d) + + Expect(result).To(BeFalse()) + }) + + It("should return true when target matches all deployments but ignore is set to true", func() { + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: DeploymentResourceKind, + ApiVersion: DeploymentResourceApiVersion, + Ignore: true, + }, + }, + }, + } + + d := appsv1.Deployment{ + TypeMeta: v1.TypeMeta{ + Kind: DeploymentResourceKind, + APIVersion: DeploymentResourceApiVersion, + }, + ObjectMeta: v1.ObjectMeta{ + Name: "foobar", + }, + } + + result := ignoreDeploymentTarget(sp, d) + + Expect(result).To(BeTrue()) + }) + + It("should return false when target matches deployment explicitly but ignore is set to false", func() { + name := "foobar" + + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: DeploymentResourceKind, + ApiVersion: DeploymentResourceApiVersion, + Name: name, + Ignore: false, + }, + }, + }, + } + + d := appsv1.Deployment{ + TypeMeta: v1.TypeMeta{ + Kind: DeploymentResourceKind, + APIVersion: DeploymentResourceApiVersion, + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + }, + } + + result := ignoreDeploymentTarget(sp, d) + + Expect(result).To(BeFalse()) + }) + + It("should return true when target matches deployment explicitly but ignore is set to true", func() { + name := "foobar" + + sp := sustainabilityv1alpha1.SavingsPolicy{ + Spec: sustainabilityv1alpha1.SavingsPolicySpec{ + Targets: []sustainabilityv1alpha1.SavingsPolicyTarget{ + { + Kind: DeploymentResourceKind, + ApiVersion: DeploymentResourceApiVersion, + Name: name, + Ignore: true, + }, + }, + }, + } + + d := appsv1.Deployment{ + TypeMeta: v1.TypeMeta{ + Kind: DeploymentResourceKind, + APIVersion: DeploymentResourceApiVersion, + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + }, + } + + result := ignoreDeploymentTarget(sp, d) + + Expect(result).To(BeTrue()) + }) + }) + }) +})