Skip to content

Commit 7318858

Browse files
committed
feat: support idling messages from core
1 parent 0fd48d6 commit 7318858

File tree

7 files changed

+312
-5
lines changed

7 files changed

+312
-5
lines changed

api/lagoon/v1beta2/lagoontask_types.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ func (b TaskType) String() string {
9696

9797
// LagoonTaskSpec defines the desired state of LagoonTask
9898
type LagoonTaskSpec struct {
99-
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
100-
// Important: Run "make" to regenerate code after modifying this file
10199
Key string `json:"key,omitempty"`
102100
Task schema.LagoonTaskInfo `json:"task,omitempty"`
103101
Project LagoonTaskProject `json:"project,omitempty"`

cmd/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
k8upv1 "github.com/k8up-io/k8up/v2/api/v1"
5151
lagoonv1beta2 "github.com/uselagoon/remote-controller/api/lagoon/v1beta2"
5252
harborctrl "github.com/uselagoon/remote-controller/internal/controllers/harbor"
53+
namespacectrl "github.com/uselagoon/remote-controller/internal/controllers/namespace"
5354
lagoonv1beta2ctrl "github.com/uselagoon/remote-controller/internal/controllers/v1beta2"
5455
"github.com/uselagoon/remote-controller/internal/messenger"
5556
k8upv1alpha1 "github.com/vshn/k8up/api/v1alpha1"
@@ -877,6 +878,20 @@ func main() {
877878

878879
c.Start()
879880

881+
setupLog.Info("starting namespace controller")
882+
// start the namespace reconciler
883+
if err = (&namespacectrl.NamespaceReconciler{
884+
Client: mgr.GetClient(),
885+
Log: ctrl.Log.WithName("namespace").WithName("Namespace"),
886+
Scheme: mgr.GetScheme(),
887+
EnableMQ: enableMQ,
888+
Messaging: messaging,
889+
LagoonTargetName: lagoonTargetName,
890+
}).SetupWithManager(mgr); err != nil {
891+
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
892+
os.Exit(1)
893+
}
894+
880895
setupLog.Info("starting build controller")
881896
// v1beta2 is the latest version
882897
if err = (&lagoonv1beta2ctrl.LagoonBuildReconciler{

config/crd/bases/crd.lagoon.sh_lagoontasks.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,6 @@ spec:
181181
- project
182182
type: object
183183
key:
184-
description: |-
185-
INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
186-
Important: Run "make" to regenerate code after modifying this file
187184
type: string
188185
misc:
189186
description: LagoonMiscInfo defines the resource or backup information
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package namespace
17+
18+
import (
19+
"context"
20+
"encoding/base64"
21+
"encoding/json"
22+
"fmt"
23+
"strconv"
24+
25+
"github.com/go-logr/logr"
26+
"github.com/uselagoon/machinery/api/schema"
27+
"github.com/uselagoon/remote-controller/internal/messenger"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
ctrl "sigs.k8s.io/controller-runtime"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
32+
corev1 "k8s.io/api/core/v1"
33+
apierrors "k8s.io/apimachinery/pkg/api/errors"
34+
)
35+
36+
// NamespaceReconciler reconciles idling
37+
type NamespaceReconciler struct {
38+
client.Client
39+
Log logr.Logger
40+
Scheme *runtime.Scheme
41+
EnableMQ bool
42+
Messaging *messenger.Messenger
43+
LagoonTargetName string
44+
}
45+
46+
type Idled struct {
47+
Idled bool `json:"idled"`
48+
}
49+
50+
func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
51+
opLog := r.Log.WithValues("namespace", req.NamespacedName)
52+
53+
var namespace corev1.Namespace
54+
if err := r.Get(ctx, req.NamespacedName, &namespace); err != nil {
55+
return ctrl.Result{}, ignoreNotFound(err)
56+
}
57+
58+
// this would be nice to be a lagoon label :)
59+
if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/idled"]; ok {
60+
idled, _ := strconv.ParseBool(val)
61+
opLog.Info(fmt.Sprintf("environment %s idle state %t", namespace.Name, idled))
62+
if r.EnableMQ {
63+
var projectName, environmentName string
64+
if p, ok := namespace.ObjectMeta.Labels["lagoon.sh/project"]; ok {
65+
projectName = p
66+
}
67+
if e, ok := namespace.ObjectMeta.Labels["lagoon.sh/environment"]; ok {
68+
environmentName = e
69+
}
70+
idling := Idled{
71+
Idled: idled,
72+
}
73+
idlingJSON, _ := json.Marshal(idling)
74+
msg := schema.LagoonMessage{
75+
Type: "idling",
76+
Namespace: namespace.Name,
77+
Meta: &schema.LagoonLogMeta{
78+
Environment: environmentName,
79+
Project: projectName,
80+
Cluster: r.LagoonTargetName,
81+
AdvancedData: base64.StdEncoding.EncodeToString(idlingJSON),
82+
},
83+
}
84+
msgBytes, err := json.Marshal(msg)
85+
if err != nil {
86+
opLog.Error(err, "Unable to encode message as JSON")
87+
}
88+
// @TODO: if we can't publish the message because for some reason, log the error and move on
89+
// this may result in the state being out of sync in lagoon but eventually will be consistent
90+
if err := r.Messaging.Publish("lagoon-tasks:controller", msgBytes); err != nil {
91+
return ctrl.Result{}, nil
92+
}
93+
}
94+
return ctrl.Result{}, nil
95+
}
96+
return ctrl.Result{}, nil
97+
}
98+
99+
// SetupWithManager sets up the watch on the namespace resource with an event filter (see predicates.go)
100+
func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
101+
return ctrl.NewControllerManagedBy(mgr).
102+
For(&corev1.Namespace{}).
103+
WithEventFilter(NamespacePredicates{}).
104+
Complete(r)
105+
}
106+
107+
// will ignore not found errors
108+
func ignoreNotFound(err error) error {
109+
if apierrors.IsNotFound(err) {
110+
return nil
111+
}
112+
return err
113+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package namespace
2+
3+
import (
4+
"sigs.k8s.io/controller-runtime/pkg/event"
5+
"sigs.k8s.io/controller-runtime/pkg/predicate"
6+
)
7+
8+
// NamespacePredicates defines the funcs for predicates
9+
type NamespacePredicates struct {
10+
predicate.Funcs
11+
}
12+
13+
// Create is used when a creation event is received by the controller.
14+
func (n NamespacePredicates) Create(e event.CreateEvent) bool {
15+
return false
16+
}
17+
18+
// Delete is used when a deletion event is received by the controller.
19+
func (n NamespacePredicates) Delete(e event.DeleteEvent) bool {
20+
return false
21+
}
22+
23+
// Update is used when an update event is received by the controller.
24+
func (n NamespacePredicates) Update(e event.UpdateEvent) bool {
25+
if oldIdled, ok := e.ObjectOld.GetLabels()["idling.amazee.io/idled"]; ok {
26+
if newIdled, ok := e.ObjectNew.GetLabels()["idling.amazee.io/idled"]; ok {
27+
if oldIdled != newIdled {
28+
return true
29+
}
30+
}
31+
}
32+
return false
33+
}
34+
35+
// Generic is used when any other event is received by the controller.
36+
func (n NamespacePredicates) Generic(e event.GenericEvent) bool {
37+
return false
38+
}

internal/messenger/consumer.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,38 @@ func (m *Messenger) Consumer(targetName string) { //error {
435435
message.Ack(false) // ack to remove from queue
436436
return
437437
}
438+
case "deploytarget:environment:idling":
439+
opLog.Info(
440+
fmt.Sprintf(
441+
"Received environment idling request for project %s, environment %s - %s",
442+
jobSpec.Project.Name,
443+
jobSpec.Environment.Name,
444+
namespace,
445+
),
446+
)
447+
// idle or unidle an environment, optionally forcible scale it so it can't be unidled by the ingress
448+
err := m.ScaleOrIdleEnvironment(ctx, opLog, namespace, jobSpec)
449+
if err != nil {
450+
//@TODO: send msg back to lagoon and update task to failed?
451+
message.Ack(false) // ack to remove from queue
452+
return
453+
}
454+
case "deploytarget:environment:service":
455+
opLog.Info(
456+
fmt.Sprintf(
457+
"Received environment service request for project %s, environment %s service - %s",
458+
jobSpec.Project.Name,
459+
jobSpec.Environment.Name,
460+
namespace,
461+
),
462+
)
463+
// idle an environment, optionally forcible scale it so it can't be unidled by the ingress
464+
err := m.EnvironmentServiceState(ctx, opLog, namespace, jobSpec)
465+
if err != nil {
466+
//@TODO: send msg back to lagoon and update task to failed?
467+
message.Ack(false) // ack to remove from queue
468+
return
469+
}
438470
default:
439471
// if we get something that we don't know about, spit out the entire message
440472
opLog.Info(

internal/messenger/tasks_handler.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"fmt"
8+
"strconv"
9+
"time"
810

11+
"github.com/go-logr/logr"
912
lagoonv1beta2 "github.com/uselagoon/remote-controller/api/lagoon/v1beta2"
1013
"github.com/uselagoon/remote-controller/internal/helpers"
14+
appsv1 "k8s.io/api/apps/v1"
15+
corev1 "k8s.io/api/core/v1"
1116
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/types"
1218
ctrl "sigs.k8s.io/controller-runtime"
1319
)
1420

@@ -92,3 +98,111 @@ func createAdvancedTask(namespace string, jobSpec *lagoonv1beta2.LagoonTaskSpec,
9298
}
9399
return nil
94100
}
101+
102+
type Idling struct {
103+
Idle bool `json:"idle"`
104+
ForceScale bool `json:"forceScale"`
105+
}
106+
107+
type Service struct {
108+
Name string `json:"name"`
109+
State string `json:"state"`
110+
}
111+
112+
func (m *Messenger) ScaleOrIdleEnvironment(ctx context.Context, opLog logr.Logger, ns string, jobSpec *lagoonv1beta2.LagoonTaskSpec) error {
113+
namespace := &corev1.Namespace{}
114+
err := m.Client.Get(ctx, types.NamespacedName{
115+
Name: ns,
116+
}, namespace)
117+
if err != nil {
118+
return err
119+
}
120+
idling := Idling{}
121+
if err := json.Unmarshal(jobSpec.Misc.MiscResource, &idling); err != nil {
122+
opLog.Error(err,
123+
"Unable to unmarshal the idling json.",
124+
)
125+
return err
126+
}
127+
if idling.Idle {
128+
if idling.ForceScale {
129+
// this would be nice to be a lagoon label :)
130+
namespace.ObjectMeta.Labels["idling.amazee.io/force-scaled"] = "true"
131+
} else {
132+
// this would be nice to be a lagoon label :)
133+
namespace.ObjectMeta.Labels["idling.amazee.io/force-idled"] = "true"
134+
}
135+
} else {
136+
// this would be nice to be a lagoon label :)
137+
namespace.ObjectMeta.Labels["idling.amazee.io/unidle"] = "true"
138+
}
139+
if err := m.Client.Update(context.Background(), namespace); err != nil {
140+
opLog.Error(err,
141+
fmt.Sprintf(
142+
"Unable to update namespace %s to set idle state.",
143+
ns,
144+
),
145+
)
146+
return err
147+
}
148+
return nil
149+
}
150+
151+
func (m *Messenger) EnvironmentServiceState(ctx context.Context, opLog logr.Logger, ns string, jobSpec *lagoonv1beta2.LagoonTaskSpec) error {
152+
deployment := &appsv1.Deployment{}
153+
service := Service{}
154+
if err := json.Unmarshal(jobSpec.Misc.MiscResource, &service); err != nil {
155+
opLog.Error(err,
156+
"Unable to unmarshal the service json.",
157+
)
158+
return err
159+
}
160+
err := m.Client.Get(ctx, types.NamespacedName{
161+
Name: service.Name,
162+
Namespace: ns,
163+
}, deployment)
164+
if err != nil {
165+
return err
166+
}
167+
update := false
168+
switch service.State {
169+
case "restart":
170+
deployment.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
171+
update = true
172+
case "stop":
173+
if *deployment.Spec.Replicas > 0 {
174+
// if the service has replicas, then save the replica count and scale it to 0
175+
deployment.ObjectMeta.Annotations["service.lagoon.sh/replicas"] = strconv.FormatInt(int64(*deployment.Spec.Replicas), 10)
176+
replicas := int32(0)
177+
deployment.Spec.Replicas = &replicas
178+
update = true
179+
}
180+
case "start":
181+
if *deployment.Spec.Replicas == 0 {
182+
// if the service has no replicas, set it back to what the previous replica value was
183+
prevReplicas, err := strconv.Atoi(deployment.ObjectMeta.Annotations["service.lagoon.sh/replicas"])
184+
if err != nil {
185+
return err
186+
}
187+
replicas := int32(prevReplicas)
188+
deployment.Spec.Replicas = &replicas
189+
delete(deployment.ObjectMeta.Annotations, "service.lagoon.sh/replicas")
190+
update = true
191+
}
192+
default:
193+
// nothing to do
194+
return nil
195+
}
196+
if update {
197+
if err := m.Client.Update(ctx, deployment); err != nil {
198+
opLog.Error(err,
199+
fmt.Sprintf(
200+
"Unable to update deployment %s to change its state.",
201+
ns,
202+
),
203+
)
204+
return err
205+
}
206+
}
207+
return nil
208+
}

0 commit comments

Comments
 (0)