diff --git a/dummy-app/k8s-manifests.yaml b/dummy-app/k8s-manifests.yaml index 3cfc867..ab46af9 100644 --- a/dummy-app/k8s-manifests.yaml +++ b/dummy-app/k8s-manifests.yaml @@ -8,11 +8,14 @@ spec: sleepTime: '10:01am' timezone: 'America/New_York' deployments: - - dummy-frontend - - dummy-db - - dummy-redis + - name: dummy-frontend + - name: dummy-db + - name: dummy-redis ingresses: - - dummy-ingress + - name: dummy-ingress + requires: + - deployment: + name: dummy-frontend --- @@ -50,6 +53,10 @@ spec: labels: app: dummy-db spec: + initContainers: + - name: sleep + image: busybox + command: ['sh', '-c', 'sleep 10'] containers: - name: mysql image: mysql:latest diff --git a/operator/api/v1beta1/sleepschedule_types.go b/operator/api/v1beta1/sleepschedule_types.go index 09cff1d..af67794 100644 --- a/operator/api/v1beta1/sleepschedule_types.go +++ b/operator/api/v1beta1/sleepschedule_types.go @@ -39,14 +39,29 @@ type SleepScheduleSpec struct { // +kubebuilder:validation:Required Timezone string `json:"timezone"` - // The names of the deployments that will be slept/woken + // The deployments that will be slept/woken. + // SHAPE: {name: "deployment-name"} // +kubebuilder:validation:Required - Deployments []string `json:"deployments,omitempty"` + Deployments []Deployment `json:"deployments,omitempty"` // The names of the ingresses that will be updated to point to the snorlax proxy // which wakes the deployments when a request is received. A copy of the originals // are stored in a configmap. - Ingresses []string `json:"ingresses,omitempty"` + // SHAPE: {name: "ingress-name", requires: [{deployment: {name: "deployment-name"}}]} + Ingresses []Ingress `json:"ingresses,omitempty"` +} + +type Deployment struct { + Name string `json:"name"` +} + +type IngressRequirement struct { + Deployment Deployment `json:"deployment"` +} + +type Ingress struct { + Name string `json:"name"` + Requires []IngressRequirement `json:"requires,omitempty"` } // SleepScheduleStatus defines the observed state of SleepSchedule diff --git a/operator/api/v1beta1/zz_generated.deepcopy.go b/operator/api/v1beta1/zz_generated.deepcopy.go index 07d0afd..9e5a4b1 100644 --- a/operator/api/v1beta1/zz_generated.deepcopy.go +++ b/operator/api/v1beta1/zz_generated.deepcopy.go @@ -24,6 +24,57 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Deployment) DeepCopyInto(out *Deployment) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Deployment. +func (in *Deployment) DeepCopy() *Deployment { + if in == nil { + return nil + } + out := new(Deployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Ingress) DeepCopyInto(out *Ingress) { + *out = *in + if in.Requires != nil { + in, out := &in.Requires, &out.Requires + *out = make([]IngressRequirement, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ingress. +func (in *Ingress) DeepCopy() *Ingress { + if in == nil { + return nil + } + out := new(Ingress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRequirement) DeepCopyInto(out *IngressRequirement) { + *out = *in + out.Deployment = in.Deployment +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRequirement. +func (in *IngressRequirement) DeepCopy() *IngressRequirement { + if in == nil { + return nil + } + out := new(IngressRequirement) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SleepSchedule) DeepCopyInto(out *SleepSchedule) { *out = *in @@ -88,13 +139,15 @@ func (in *SleepScheduleSpec) DeepCopyInto(out *SleepScheduleSpec) { *out = *in if in.Deployments != nil { in, out := &in.Deployments, &out.Deployments - *out = make([]string, len(*in)) + *out = make([]Deployment, len(*in)) copy(*out, *in) } if in.Ingresses != nil { in, out := &in.Ingresses, &out.Ingresses - *out = make([]string, len(*in)) - copy(*out, *in) + *out = make([]Ingress, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } diff --git a/operator/config/crd/bases/snorlax.moonbeam.nyc_sleepschedules.yaml b/operator/config/crd/bases/snorlax.moonbeam.nyc_sleepschedules.yaml index 458f5a8..981d767 100644 --- a/operator/config/crd/bases/snorlax.moonbeam.nyc_sleepschedules.yaml +++ b/operator/config/crd/bases/snorlax.moonbeam.nyc_sleepschedules.yaml @@ -39,17 +39,44 @@ spec: spec: properties: deployments: - description: The names of the deployments that will be slept/woken + description: |- + The deployments that will be slept/woken. + SHAPE: {name: "deployment-name"} items: - type: string + properties: + name: + type: string + required: + - name + type: object type: array ingresses: description: |- The names of the ingresses that will be updated to point to the snorlax proxy which wakes the deployments when a request is received. A copy of the originals are stored in a configmap. + SHAPE: {name: "ingress-name", requires: [{deployment: {name: "deployment-name"}}]} items: - type: string + properties: + name: + type: string + requires: + items: + properties: + deployment: + properties: + name: + type: string + required: + - name + type: object + required: + - deployment + type: object + type: array + required: + - name + type: object type: array sleepTime: description: The time that the deployment will start sleeping diff --git a/operator/internal/controller/sleepschedule_controller.go b/operator/internal/controller/sleepschedule_controller.go index e3b10b1..ed7d74f 100644 --- a/operator/internal/controller/sleepschedule_controller.go +++ b/operator/internal/controller/sleepschedule_controller.go @@ -41,6 +41,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +type key string + // SleepScheduleReconciler reconciles a SleepSchedule object type SleepScheduleReconciler struct { client.Client @@ -109,6 +111,24 @@ func (r *SleepScheduleReconciler) ProcessSleepSchedule(ctx context.Context, slee return sleepScheduleData, nil } +func (r *SleepScheduleReconciler) waitForRequirementsToBeReady(ctx context.Context, ing *snorlaxv1beta1.Ingress) { + sleepSchedule := ctx.Value(key("sleepSchedule")).(*snorlaxv1beta1.SleepSchedule) + + var requirements []snorlaxv1beta1.IngressRequirement + if len(ing.Requires) > 0 { + requirements = ing.Requires + } else { + requirements = make([]snorlaxv1beta1.IngressRequirement, len(sleepSchedule.Spec.Deployments)) + for i, deployment := range sleepSchedule.Spec.Deployments { + requirements[i] = snorlaxv1beta1.IngressRequirement{Deployment: deployment} + } + } + + for _, req := range requirements { + r.waitForDeploymentToWake(ctx, sleepSchedule.Namespace, req.Deployment.Name) + } +} + func (r *SleepScheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) @@ -119,6 +139,9 @@ func (r *SleepScheduleReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, client.IgnoreNotFound(err) } + // Put the SleepSchedule in the context + ctx = context.WithValue(ctx, key("sleepSchedule"), sleepSchedule) + objectName := fmt.Sprintf("snorlax-%s", sleepSchedule.Name) // Load sleep schedule data @@ -201,9 +224,11 @@ func (r *SleepScheduleReconciler) Reconcile(ctx context.Context, req ctrl.Reques if awake && shouldSleep && !wakeRequestReceived { log.Info("Going to sleep") r.sleep(ctx, sleepSchedule) + log.Info("Successfully asleep") } else if !awake && (!shouldSleep || wakeRequestReceived) { log.Info("Waking up") r.wake(ctx, sleepSchedule) + log.Info("Successfully awake") } // If the app should be awake, clear the sleep data @@ -254,9 +279,9 @@ func (r *SleepScheduleReconciler) isAppAwake(ctx context.Context, sleepSchedule } // Return false if any deployment has 0 replicas - for _, deploymentName := range sleepSchedule.Spec.Deployments { + for _, deploy := range sleepSchedule.Spec.Deployments { deployment := &appsv1.Deployment{} - err := r.Get(ctx, client.ObjectKey{Namespace: sleepSchedule.Namespace, Name: deploymentName}, deployment) + err := r.Get(ctx, client.ObjectKey{Namespace: sleepSchedule.Namespace, Name: deploy.Name}, deployment) if err != nil { return false, err } @@ -272,7 +297,7 @@ func (r *SleepScheduleReconciler) isAppAwake(ctx context.Context, sleepSchedule func (r *SleepScheduleReconciler) wake(ctx context.Context, sleepSchedule *snorlaxv1beta1.SleepSchedule) error { // Scale up each deployment var wg sync.WaitGroup - for _, deploymentName := range sleepSchedule.Spec.Deployments { + for _, deploy := range sleepSchedule.Spec.Deployments { wg.Add(1) go func(name string) { defer wg.Done() @@ -287,21 +312,37 @@ func (r *SleepScheduleReconciler) wake(ctx context.Context, sleepSchedule *snorl // Wake and wait for the deployment r.scaleDeployment(ctx, sleepSchedule.Namespace, name, replicas) r.waitForDeploymentToWake(ctx, sleepSchedule.Namespace, name) - }(deploymentName) + }(deploy.Name) } - // Wait for all deployments to finish scaling up - wg.Wait() + // Have each ingress wait for its requirements and load the copy + for _, ing := range sleepSchedule.Spec.Ingresses { + wg.Add(1) + go func(ing snorlaxv1beta1.Ingress) error { + defer wg.Done() - // Load the ingress copies - for _, ingressName := range sleepSchedule.Spec.Ingresses { - err := r.loadIngressCopy(ctx, sleepSchedule, ingressName) - if err != nil { - log.FromContext(ctx).Error(err, "Failed to load Ingress copy") - return err - } + // Wait 2 seconds for the deployments to start scaling + time.Sleep(2 * time.Second) + + // Wait for all requirements to be ready + r.waitForRequirementsToBeReady(ctx, &ing) + + // Load the ingress copy + err := r.loadIngressCopy(ctx, sleepSchedule, ing.Name) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to load Ingress copy") + return err + } + + log.FromContext(ctx).Info(fmt.Sprintf("Ingress restored: %s", ing.Name)) + + return nil + }(ing) } + // Wait for all deployments and ingresses to wake + wg.Wait() + // Delete the Snorlax proxy err := r.DeleteSnorlaxProxy(ctx, sleepSchedule) if err != nil { @@ -317,15 +358,15 @@ func (r *SleepScheduleReconciler) sleep(ctx context.Context, sleepSchedule *snor r.deploySnorlaxProxy(ctx, sleepSchedule) // Point each ingress to the Snorlax proxy - for _, ingressName := range sleepSchedule.Spec.Ingresses { - r.takeIngressCopy(ctx, sleepSchedule, ingressName) - r.pointIngressToSnorlax(ctx, sleepSchedule, ingressName) + for _, ing := range sleepSchedule.Spec.Ingresses { + r.takeIngressCopy(ctx, sleepSchedule, ing.Name) + r.pointIngressToSnorlax(ctx, sleepSchedule, ing.Name) } // Scale down each deployment - for _, deploymentName := range sleepSchedule.Spec.Deployments { - r.storeCurrentReplicas(ctx, sleepSchedule, deploymentName) - r.scaleDeployment(ctx, sleepSchedule.Namespace, deploymentName, 0) + for _, deploy := range sleepSchedule.Spec.Deployments { + r.storeCurrentReplicas(ctx, sleepSchedule, deploy.Name) + r.scaleDeployment(ctx, sleepSchedule.Namespace, deploy.Name, 0) } }