Skip to content

Commit

Permalink
feat: support idling messages from core
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon committed Dec 19, 2023
1 parent 0e99812 commit d5da4ad
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions apis/lagoon/v1beta1/lagoonmessaging_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type LagoonMessage struct {
Type string `json:"type,omitempty"`
Namespace string `json:"namespace,omitempty"`
Meta *LagoonLogMeta `json:"meta,omitempty"`
Idled bool `json:"idled,omitempty"`
// BuildInfo *LagoonBuildInfo `json:"buildInfo,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions apis/lagoon/v1beta1/lagoontask_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type LagoonTaskSpec struct {
Environment LagoonTaskEnvironment `json:"environment,omitempty"`
Misc *LagoonMiscInfo `json:"misc,omitempty"`
AdvancedTask *LagoonAdvancedTaskInfo `json:"advancedTask,omitempty"`
ForceScale bool `json:"forceScale,omitempty"`
}

// LagoonTaskInfo defines what a task can use to communicate with Lagoon via SSH/API.
Expand Down
2 changes: 2 additions & 0 deletions config/crd/bases/crd.lagoon.sh_lagoonbuilds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ spec:
description: LagoonMessage is used for sending build info back to
Lagoon messaging queue to update the environment or deployment
properties:
idled:
type: boolean
meta:
description: LagoonLogMeta is the metadata that is used by logging
in Lagoon.
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/crd.lagoon.sh_lagoontasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ spec:
- name
- project
type: object
forceScale:
type: boolean
key:
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file'
Expand Down Expand Up @@ -284,6 +286,8 @@ spec:
description: LagoonMessage is used for sending build info back to
Lagoon messaging queue to update the environment or deployment
properties:
idled:
type: boolean
meta:
description: LagoonLogMeta is the metadata that is used by logging
in Lagoon.
Expand Down
104 changes: 104 additions & 0 deletions controllers/namespace/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
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 namespace

import (
"context"
"encoding/json"
"fmt"
"strconv"

"github.com/go-logr/logr"
lagoonv1beta1 "github.com/uselagoon/remote-controller/apis/lagoon/v1beta1"
"github.com/uselagoon/remote-controller/internal/messenger"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)

// NamespaceReconciler reconciles idling
type NamespaceReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
EnableMQ bool
Messaging *messenger.Messenger
LagoonTargetName string
}

func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
opLog := r.Log.WithValues("namespace", req.NamespacedName)

var namespace corev1.Namespace
if err := r.Get(ctx, req.NamespacedName, &namespace); err != nil {
return ctrl.Result{}, ignoreNotFound(err)
}

// this would be nice to be a lagoon label :)
if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/idled"]; ok {
idled, _ := strconv.ParseBool(val)
opLog.Info(fmt.Sprintf("environment %s idle state %t", namespace.Name, idled))
if r.EnableMQ {
var projectName, environmentName string
if p, ok := namespace.ObjectMeta.Labels["lagoon.sh/project"]; ok {
projectName = p
}
if e, ok := namespace.ObjectMeta.Labels["lagoon.sh/environment"]; ok {
environmentName = e
}
msg := lagoonv1beta1.LagoonMessage{
Type: "idling",
Namespace: namespace.Name,
Meta: &lagoonv1beta1.LagoonLogMeta{
Environment: environmentName,
Project: projectName,
Cluster: r.LagoonTargetName,
},
Idled: idled,
}
msgBytes, err := json.Marshal(msg)
if err != nil {
opLog.Error(err, "Unable to encode message as JSON")
}
// @TODO: if we can't publish the message because for some reason, log the error and move on
// this may result in the state being out of sync in lagoon but eventually will be consistent
if err := r.Messaging.Publish("lagoon-tasks:controller", msgBytes); err != nil {
return ctrl.Result{}, nil
}
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, nil
}

// SetupWithManager sets up the watch on the namespace resource with an event filter (see predicates.go)
func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Namespace{}).
WithEventFilter(NamespacePredicates{}).
Complete(r)
}

// will ignore not found errors
func ignoreNotFound(err error) error {
if apierrors.IsNotFound(err) {
return nil
}
return err
}
38 changes: 38 additions & 0 deletions controllers/namespace/predicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package namespace

import (
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// NamespacePredicates defines the funcs for predicates
type NamespacePredicates struct {
predicate.Funcs
}

// Create is used when a creation event is received by the controller.
func (n NamespacePredicates) Create(e event.CreateEvent) bool {
return false
}

// Delete is used when a deletion event is received by the controller.
func (n NamespacePredicates) Delete(e event.DeleteEvent) bool {
return false
}

// Update is used when an update event is received by the controller.
func (n NamespacePredicates) Update(e event.UpdateEvent) bool {
if oldIdled, ok := e.ObjectOld.GetLabels()["idling.amazee.io/idled"]; ok {
if newIdled, ok := e.ObjectNew.GetLabels()["idling.amazee.io/idled"]; ok {
if oldIdled != newIdled {
return true
}
}
}
return false
}

// Generic is used when any other event is received by the controller.
func (n NamespacePredicates) Generic(e event.GenericEvent) bool {
return false
}
32 changes: 32 additions & 0 deletions internal/messenger/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,38 @@ func (m *Messenger) Consumer(targetName string) { //error {
message.Ack(false) // ack to remove from queue
return
}
case "deploytarget:idle:environment":
opLog.Info(
fmt.Sprintf(
"Received environment idling request for project %s, environment %s - %s",
jobSpec.Project.Name,
jobSpec.Environment.Name,
namespace,
),
)
// idle an environment, optionally forcible scale it so it can't be unidled by the ingress
err := m.IdleEnvironment(ctx, opLog, namespace, jobSpec.ForceScale)
if err != nil {
//@TODO: send msg back to lagoon and update task to failed?
message.Ack(false) // ack to remove from queue
return
}
case "deploytarget:unidle:environment":
opLog.Info(
fmt.Sprintf(
"Received environment unidling request for project %s, environment %s - %s",
jobSpec.Project.Name,
jobSpec.Environment.Name,
namespace,
),
)
// unidle an environment
err := m.UnidleEnvironment(ctx, opLog, namespace)
if err != nil {
//@TODO: send msg back to lagoon and update task to failed?
message.Ack(false) // ack to remove from queue
return
}
default:
// if we get something that we don't know about, spit out the entire message
opLog.Info(
Expand Down
49 changes: 49 additions & 0 deletions internal/messenger/tasks_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,52 @@ func createAdvancedTask(namespace string, jobSpec *lagoonv1beta1.LagoonTaskSpec,
}
return nil
}

func (m *Messenger) IdleEnvironment(ctx context.Context, opLog logr.Logger, ns string, forceScale bool) error {
namespace := &corev1.Namespace{}
err := m.Client.Get(ctx, types.NamespacedName{
Name: ns,
}, namespace)
if err != nil {
return err
}
if forceScale {
// this would be nice to be a lagoon label :)
namespace.ObjectMeta.Labels["idling.amazee.io/force-scaled"] = "true"
} else {
// this would be nice to be a lagoon label :)
namespace.ObjectMeta.Labels["idling.amazee.io/force-idled"] = "true"
}
if err := m.Client.Update(context.Background(), namespace); err != nil {
opLog.Error(err,
fmt.Sprintf(
"Unable to update namespace %s to idle it.",
ns,
),
)
return err
}
return nil
}

func (m *Messenger) UnidleEnvironment(ctx context.Context, opLog logr.Logger, ns string) error {
namespace := &corev1.Namespace{}
err := m.Client.Get(ctx, types.NamespacedName{
Name: ns,
}, namespace)
if err != nil {
return err
}
// this would be nice to be a lagoon label :)
namespace.ObjectMeta.Labels["idling.amazee.io/unidle"] = "true"
if err := m.Client.Update(context.Background(), namespace); err != nil {
opLog.Error(err,
fmt.Sprintf(
"Unable to update namespace %s to unidle it.",
ns,
),
)
return err
}
return nil
}
13 changes: 13 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/hashicorp/golang-lru/v2/expirable"
k8upv1 "github.com/k8up-io/k8up/v2/api/v1"
lagoonv1beta1 "github.com/uselagoon/remote-controller/apis/lagoon/v1beta1"
"github.com/uselagoon/remote-controller/controllers/namespace"
lagoonv1beta1ctrl "github.com/uselagoon/remote-controller/controllers/v1beta1"
"github.com/uselagoon/remote-controller/internal/messenger"
k8upv1alpha1 "github.com/vshn/k8up/api/v1alpha1"
Expand Down Expand Up @@ -852,6 +853,18 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "LagoonTask")
os.Exit(1)
}
// start the namespace reconciler
if err = (&namespace.NamespaceReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("namespace").WithName("Namespace"),
Scheme: mgr.GetScheme(),
EnableMQ: enableMQ,
Messaging: messaging,
LagoonTargetName: lagoonTargetName,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}

// for now the namespace reconciler only needs to run if harbor is enabled so that we can watch the namespace for rotation label events
if lffHarborEnabled {
Expand Down

0 comments on commit d5da4ad

Please sign in to comment.