Skip to content

Commit

Permalink
chore: lagoonise aergia
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon committed Jul 10, 2024
1 parent 8cc72df commit 0e7ce97
Show file tree
Hide file tree
Showing 18 changed files with 308 additions and 151 deletions.
2 changes: 1 addition & 1 deletion PROJECT
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
domain: amazee.io
domain: lagoon.sh
repo: github.com/uselagoon/aergia-controller
version: "2"
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,39 @@ This backend is designed to serve generic error handling for any http error. The
An environment can be force idled, force scaled, or unidled using labels on the namespace. All actions still respect the label selectors, but forced actions will bypass any hits checks

### Force Idled
To force idle a namespace, you can label the namespace using `idling.amazee.io/force-idled=true`. This will cause the environment to be immediately scaled down, but the next request to the ingress in the namespace will unidle the namespace
To force idle a namespace, you can label the namespace using `idling.lagoon.sh/force-idled=true`. This will cause the environment to be immediately scaled down, but the next request to the ingress in the namespace will unidle the namespace

### Force Scaled
To force scale a namespace, you can label the namespace using `idling.amazee.io/force-scaled=true`. This will cause the environment to be immediately scaled down, but the next request to the ingress in the namespace will *NOT* unidle the namespace. A a deployment will be required to unidle this namespace
To force scale a namespace, you can label the namespace using `idling.lagoon.sh/force-scaled=true`. This will cause the environment to be immediately scaled down, but the next request to the ingress in the namespace will *NOT* unidle the namespace. A a deployment will be required to unidle this namespace

### Unidle
To unidle a namespace, you can label the namespace using `idling.amazee.io/unidle=true`. This will cause the environment to be scaled back up to its previous state.
To unidle a namespace, you can label the namespace using `idling.lagoon.sh/unidle=true`. This will cause the environment to be scaled back up to its previous state.

### Idled
A label `idling.amazee.io/idled` is set that will be true or false depending on if the environment is idled. This ideally should not be modified as Aergia will update it as required.
A label `idling.lagoon.sh/idled` is set that will be true or false depending on if the environment is idled. This ideally should not be modified as Aergia will update it as required.

### Namespace Idling Overrides
If you want to change a namespaces interval check times outside of the globally applied intervals, the following annotations can be added to the namespace
* `idling.amazee.io/prometheus-interval` - set this to the time interval for prometheus checks, the format must be in [30m|4h|1h30m](https://pkg.go.dev/time#ParseDuration) notation
* `idling.amazee.io/pod-interval` - set this to the time interval for pod uptime checks, the format must be in [30m|4h|1h30m](https://pkg.go.dev/time#ParseDuration) notation
* `idling.lagoon.sh/prometheus-interval` - set this to the time interval for prometheus checks, the format must be in [30m|4h|1h30m](https://pkg.go.dev/time#ParseDuration) notation
* `idling.lagoon.sh/pod-interval` - set this to the time interval for pod uptime checks, the format must be in [30m|4h|1h30m](https://pkg.go.dev/time#ParseDuration) notation

### IP Allow/Block Lists
It is possible to add global IP allow and block lists, the helm chart will have support for handling this creation
* allowing IP addresses via `/lists/allowedips` file which is a single line per entry of ip address to allow
* blocking IP addresses via `/lists/blockedips` file which is a single line per entry of ip address to block

There are also annotations that can be added to the namespace, or individual `Kind: Ingress` objects that allow for ip allow or blocking.
* `idling.amazee.io/ip-allow-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this.
* `idling.amazee.io/ip-block-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this.
* `idling.lagoon.sh/ip-allow-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this.
* `idling.lagoon.sh/ip-block-list` - a comma separated list of ip addresses to allow, will be checked against x-forward-for, but if true-client-ip is provided it will prefer this.

### UserAgent Allow/Block Lists
It is possible to add global UserAgent allow and block lists, the helm chart will have support for handling this creation
* allowing user agents via a `/lists/allowedagents` file which is a single line per entry of useragents or regex patterns to match against. These must be `go` based regular expressions.
* blocking user agents via a `/lists/blockedagents` file which is a single line per entry of useragents or regex patterns to match against. These must be `go` based regular expressions.

There are also annotations that can be added to the namespace, or individual `Kind: Ingress` objects that allow for user agent allow or blocking.
* `idling.amazee.io/allowed-agents` - a comma separated list of user agents or regex patterns to allow.
* `idling.amazee.io/blocked-agents` - a comma separated list of user agents or regex patterns to block.
* `idling.lagoon.sh/allowed-agents` - a comma separated list of user agents or regex patterns to allow.
* `idling.lagoon.sh/blocked-agents` - a comma separated list of user agents or regex patterns to block.

### Verify Unidling Requests
It is possible to start Aergia in a mode where it will require unidling requests to be verified. The way this works is by using HMAC and passing the signed version of the requested namespace back to the user when the initial request to unidle the environment is received. When a client loads this page, it will execute a javascript query back to the requested ingress which is then verified by Aergia. If verification suceeds, it proceeds to unidle the environment. This functionality can be useful to prevent bots and other systems that don't have the ability to execute javascript from unidling environments uncessarily. The signed namespace value will only work for the requested namespace.
Expand All @@ -55,7 +55,7 @@ To enable this functionality, set the following:
- `--verify-secret=use-your-own-secret` or envvar `VERIFY_SECRET=use-your-own-secret`

If the verification feature is enabled, and you need to unidle environments using tools that can't execute javascript, then it is possible to allow a namespace to override the feature by adding the following annotation to the namespace. Using the other allow/blocking mechanisms can then be used to restrict how the environment can unidle if required.
* `idling.amazee.io/disable-request-verification=true` - set this to disable the hmac verification on a namespace if Aergia has unidling request verification turned on. This annotation is also supported on an ingress too, so that specific ingress can skip the verification requests.
* `idling.lagoon.sh/disable-request-verification=true` - set this to disable the hmac verification on a namespace if Aergia has unidling request verification turned on. This annotation is also supported on an ingress too, so that specific ingress can skip the verification requests.

If you're using custom template overrides and enable this functionality, you will need to extend your `unidle.html` template with the additional changes to allow it to to perform the call back function or else environments will never unidle. See the bundled `unidle.html` file to see how this may differ from your custom templates.

Expand All @@ -67,7 +67,7 @@ This could be done using a configmap and volume mount to any directory, then upd

# Installation

Install via helm (https://github.com/amazeeio/charts/tree/main/charts/aergia)
Install via lagoon-remote helm chart.

## Custom templates
If installing via helm, you can use this YAML in your values.yaml file and define the templates there.
Expand Down
6 changes: 3 additions & 3 deletions controller-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ echo -e "${GREEN}Check that force-idle label idles an environment${NOCOLOR}"
kubectl -n example-nginx get pods
echo -e "${GREEN}Request example-nginx app (should be 200)${NOCOLOR}"
if curl -s -I -H "Host: aergia.localhost" http://localhost:8090/| grep -q "200 OK"; then
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.amazee.io/force-idled":"true"}}}'
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.lagoon.sh/force-idled":"true"}}}'
sleep 15
echo -e "${GREEN}Check there are 0 example-nginx pods${NOCOLOR}"
kubectl -n example-nginx get pods
Expand Down Expand Up @@ -95,11 +95,11 @@ echo -e "${GREEN}Check that an idled environment can be unidled by label${NOCOLO
kubectl -n example-nginx get pods
echo -e "${GREEN}Request example-nginx app (should be 200)${NOCOLOR}"
if curl -s -I -H "Host: aergia.localhost" http://localhost:8090/| grep -q "200 OK"; then
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.amazee.io/force-idled":"true"}}}'
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.lagoon.sh/force-idled":"true"}}}'
sleep 15
echo -e "${GREEN}Check there are 0 example-nginx pods${NOCOLOR}"
kubectl -n example-nginx get pods
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.amazee.io/unidle":"true"}}}'
kubectl patch namespace example-nginx --type=merge --patch '{"metadata":{"labels":{"idling.lagoon.sh/unidle":"true"}}}'
sleep 15
echo -e "${GREEN}Check there are 3 example-nginx pods${NOCOLOR}"
kubectl -n example-nginx get pods
Expand Down
113 changes: 107 additions & 6 deletions controllers/idling_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import (
"github.com/go-logr/logr"
"github.com/uselagoon/aergia-controller/handlers/idler"
"github.com/uselagoon/aergia-controller/handlers/unidler"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

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

Expand All @@ -49,13 +52,13 @@ func (r *IdlingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, ignoreNotFound(err)
}

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/force-scaled"]; ok && val == "true" {
if val, ok := namespace.ObjectMeta.Labels["idling.lagoon.sh/force-scaled"]; ok && val == "true" {
opLog.Info(fmt.Sprintf("Force scaling environment %s", namespace.Name))
r.Idler.KubernetesServiceIdler(ctx, opLog, namespace, namespace.ObjectMeta.Labels[r.Idler.Selectors.NamespaceSelectorsLabels.ProjectName], false, true)
nsMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]*string{
"idling.amazee.io/force-scaled": nil,
"idling.lagoon.sh/force-scaled": nil,
},
},
})
Expand All @@ -66,13 +69,13 @@ func (r *IdlingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, nil
}

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/force-idled"]; ok && val == "true" {
if val, ok := namespace.ObjectMeta.Labels["idling.lagoon.sh/force-idled"]; ok && val == "true" {
opLog.Info(fmt.Sprintf("Force idling environment %s", namespace.Name))
r.Idler.KubernetesServiceIdler(ctx, opLog, namespace, namespace.ObjectMeta.Labels[r.Idler.Selectors.NamespaceSelectorsLabels.ProjectName], true, false)
nsMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]*string{
"idling.amazee.io/force-idled": nil,
"idling.lagoon.sh/force-idled": nil,
},
},
})
Expand All @@ -83,13 +86,13 @@ func (r *IdlingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, nil
}

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/unidle"]; ok && val == "true" {
if val, ok := namespace.ObjectMeta.Labels["idling.lagoon.sh/unidle"]; ok && val == "true" {
opLog.Info(fmt.Sprintf("Unidling environment %s", namespace.Name))
r.Unidler.Unidle(ctx, &namespace, opLog)
nsMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]*string{
"idling.amazee.io/unidle": nil,
"idling.lagoon.sh/unidle": nil,
},
},
})
Expand All @@ -99,6 +102,104 @@ func (r *IdlingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
}
return ctrl.Result{}, nil
}

/*
convert old labels or annotations
*/
// ingress
labelRequirements, _ := labels.NewRequirement("idling.amazee.io/idled", selection.Exists, []string{})
listOption := (&client.ListOptions{}).ApplyOptions([]client.ListOption{
client.InNamespace(namespace.Name),
client.MatchingLabelsSelector{
Selector: labels.NewSelector().Add(*labelRequirements),
},
})
ingressList := &networkv1.IngressList{}
if err := r.List(ctx, ingressList, listOption); err != nil {
opLog.Error(err, fmt.Sprintf("Error getting ingress for namespace %s", namespace.ObjectMeta.Name))
} else {
for _, ingress := range ingressList.Items {
ingressPatchAnnotations := map[string]interface{}{}
ingressMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": ingressPatchAnnotations,
},
})
if val, ok := ingress.ObjectMeta.Annotations["idling.amazee.io/allowed-agents"]; ok {
ingressPatchAnnotations["idling.lagoon.sh/allowed-agents"] = val
ingressPatchAnnotations["idling.amazee.io/allowed-agents"] = nil
}
if val, ok := ingress.ObjectMeta.Annotations["idling.amazee.io/blocked-agents"]; ok {
ingressPatchAnnotations["idling.lagoon.sh/blocked-agents"] = val
ingressPatchAnnotations["idling.amazee.io/blocked-agents"] = nil
}
if val, ok := ingress.ObjectMeta.Annotations["idling.amazee.io/ip-allow-agents"]; ok {
ingressPatchAnnotations["idling.lagoon.sh/ip-allow-agents"] = val
ingressPatchAnnotations["idling.amazee.io/ip-allow-agents"] = nil
}
if val, ok := ingress.ObjectMeta.Annotations["idling.amazee.io/ip-block-agents"]; ok {
ingressPatchAnnotations["idling.lagoon.sh/ip-block-agents"] = val
ingressPatchAnnotations["idling.amazee.io/ip-block-agents"] = nil
}
if val, ok := ingress.ObjectMeta.Annotations["idling.amazee.io/disable-request-verification"]; ok {
ingressPatchAnnotations["idling.lagoon.sh/allowed-agents"] = val
ingressPatchAnnotations["idling.amazee.io/allowed-agents"] = nil
}
if len(ingressPatchAnnotations) > 0 {
patchIngress := ingress.DeepCopy()
if err := r.Patch(ctx, patchIngress, client.RawPatch(types.MergePatchType, ingressMergePatch)); err != nil {
// log it but try and scale the rest of the deployments anyway (some idled is better than none?)
opLog.Info(fmt.Sprintf("Error patching ingress %s -%v", patchIngress.Name, err))
}
}
}
}
// namespace labels and anotations
nsPatchLabels := map[string]interface{}{}
nsPatchAnnotations := map[string]interface{}{}
nsMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"labels": nsPatchLabels,
"annotations": nsPatchAnnotations,
},
})
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/disable-request-verification"]; ok {
nsPatchAnnotations["idling.lagoon.sh/disable-request-verification"] = val
nsPatchAnnotations["idling.amazee.io/disable-request-verification"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/allowed-agents"]; ok {
nsPatchAnnotations["idling.lagoon.sh/allowed-agents"] = val
nsPatchAnnotations["idling.amazee.io/allowed-agents"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/blocked-agents"]; ok {
nsPatchAnnotations["idling.lagoon.sh/blocked-agents"] = val
nsPatchAnnotations["idling.amazee.io/blocked-agents"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/ip-allow-agents"]; ok {
nsPatchAnnotations["idling.lagoon.sh/ip-allow-agents"] = val
nsPatchAnnotations["idling.amazee.io/ip-allow-agents"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/ip-block-agents"]; ok {
nsPatchAnnotations["idling.lagoon.sh/ip-block-agents"] = val
nsPatchAnnotations["idling.amazee.io/ip-block-agents"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/prometheus-interval"]; ok {
nsPatchAnnotations["idling.lagoon.sh/prometheus-interval"] = val
nsPatchAnnotations["idling.amazee.io/prometheus-interval"] = nil
}
if val, ok := namespace.ObjectMeta.Annotations["idling.amazee.io/pod-interval"]; ok {
nsPatchAnnotations["idling.lagoon.sh/pod-interval"] = val
nsPatchAnnotations["idling.amazee.io/pod-interval"] = nil
}
if len(nsPatchLabels) > 0 || len(nsPatchAnnotations) > 0 {
if err := r.Patch(ctx, &namespace, client.RawPatch(types.MergePatchType, nsMergePatch)); err != nil {
// log it but try and scale the rest of the deployments anyway (some idled is better than none?)
opLog.Info(fmt.Sprintf("Error patching namespace %s -%v", namespace.Name, err))
}
}
/*
convert old labels
*/
return ctrl.Result{}, nil
}

Expand Down
6 changes: 3 additions & 3 deletions handlers/idler/cli-kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (h *Idler) kubernetesCLI(ctx context.Context, opLog logr.Logger, namespace
} else {
for _, build := range builds.Items {
if build.Status.Phase == "Running" {
opLog.Info(fmt.Sprintf("Environment has running build, skipping"))
opLog.Info("Environment has running build, skipping")
runningBuild = true
break
}
Expand All @@ -53,7 +53,7 @@ func (h *Idler) kubernetesCLI(ctx context.Context, opLog logr.Logger, namespace
})
deployments := &appsv1.DeploymentList{}
if err := h.Client.List(ctx, deployments, listOption); err != nil {
opLog.Error(err, fmt.Sprintf("Error getting deployments"))
opLog.Error(err, "Error getting deployments")
} else {
for _, deployment := range deployments.Items {
// if we have any services=cli, act on them
Expand Down Expand Up @@ -94,7 +94,7 @@ func (h *Idler) kubernetesCLI(ctx context.Context, opLog logr.Logger, namespace
},
})
if err := h.Client.List(ctx, pods, listOption); err != nil {
opLog.Error(err, fmt.Sprintf("Error listing pods"))
opLog.Error(err, "Error listing pods")
} else {
for _, pod := range pods.Items {
processCount := 0
Expand Down
4 changes: 2 additions & 2 deletions handlers/idler/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (h *Idler) CLIIdler() {
})
namespaces := &corev1.NamespaceList{}
if err := h.Client.List(ctx, namespaces, listOption); err != nil {
opLog.Error(err, fmt.Sprintf("unable to get any namespaces"))
opLog.Error(err, "unable to get any namespaces")
return
}
for _, namespace := range namespaces.Items {
Expand All @@ -40,7 +40,7 @@ func (h *Idler) CLIIdler() {
WithValues("project", namespace.ObjectMeta.Labels[h.Selectors.NamespaceSelectorsLabels.ProjectName]).
WithValues("environment", namespace.ObjectMeta.Labels[h.Selectors.NamespaceSelectorsLabels.EnvironmentName]).
WithValues("dry-run", h.DryRun)
envOpLog.Info(fmt.Sprintf("Checking namespace"))
envOpLog.Info("Checking namespace")
h.kubernetesCLI(ctx, envOpLog, namespace)
} else {
if h.Debug {
Expand Down
3 changes: 2 additions & 1 deletion handlers/idler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package idler

import (
"bytes"
"context"
"fmt"
"io"
"time"
Expand Down Expand Up @@ -121,7 +122,7 @@ func execPod(
}

var stdout, stderr bytes.Buffer
err = exec.Stream(remotecommand.StreamOptions{
err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdin: stdin,
Stdout: &stdout,
Stderr: &stderr,
Expand Down
Loading

0 comments on commit 0e7ce97

Please sign in to comment.