Skip to content

Commit

Permalink
Merge pull request #19 from amazeeio/add-controller
Browse files Browse the repository at this point in the history
  • Loading branch information
shreddedbacon authored May 8, 2023
2 parents 8169ff4 + 8ba9fbf commit 0000b0b
Show file tree
Hide file tree
Showing 20 changed files with 958 additions and 383 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/aergia-controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Setup correct Go version
uses: actions/setup-go@v2
with:
go-version: '1.16'
go-version: '1.18'
- name: Install kustomize, kubebuilder, helm
run: |
#kubebuilder
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.16-alpine3.13 as builder
FROM golang:1.18-alpine as builder

WORKDIR /workspace
# Copy the Go Modules manifests
Expand All @@ -13,6 +13,7 @@ RUN go mod download
COPY main.go main.go
COPY metrics.go metrics.go
COPY handlers/ handlers/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go metrics.go
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test: generate fmt vet manifests

# Build manager binary
manager: generate fmt vet
go build -o bin/manager main.go
go build -o bin/manager main.go metrics.go

# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate fmt vet manifests
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ This controller replaces the ingress-nginx default backend with this custom back

This backend is designed to serve generic error handling for any http error. The backend can also leverage [custom errors](https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/), which can be used to check the kubernetes api to see if the namespace needs to be scaled from zero.

## Usage

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

### 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

### 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.

## Change the default templates

By using the environment variable `ERROR_FILES_PATH`, and pointing to a location that contains the two templates `error.html` and `unidle.html`, you can change what is shown to the end user.
By using the environment variable `ERROR_FILES_PATH`, and pointing to a location that contains the three templates `error.html`, `forced.html`, and `unidle.html`, you can change what is shown to the end user.

This could be done using a configmap and volume mount to any directory, then update the `ERROR_FILES_PATH` to this directory.

Expand All @@ -21,7 +34,7 @@ Install via helm (https://github.com/amazeeio/charts/tree/main/charts/aergia)
## Custom templates
If installing via helm, you can use this YAML in your values.yaml file and define the templates there.

> See `www/error.html` and `www/unidle.html` for inspiration
> See `www/error.html`, `www/force.html`, and `www/unidle.html` for inspiration
```
templates:
Expand All @@ -45,10 +58,21 @@ templates:
</body>
</html>
{{end}}
forced: |
{{define "base"}}
<html>
<head>
<meta http-equiv="refresh" content="{{ .RefreshInterval }}">
</head>
<body>
{{ .ErrorCode }} {{ .ErrorMessage }}
</body>
</html>
{{end}}
```

## Prometheus
The idler uses prometheus to check if there has been hits to the ingress in the last defined interval
The idler uses prometheus to check if there has been hits to the ingress in the last defined interval, it only checks status codes of 200.
By default it will talk to a prometheus in cluster `http://monitoring-kube-prometheus-prometheus.monitoring.svc:9090` but this is adjustable with a flag (and via helm values).

### Requirements
Expand Down
7 changes: 7 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ rules:
verbs:
- get
- list
- patch
- watch
- apiGroups:
- ""
Expand Down Expand Up @@ -53,6 +54,12 @@ rules:
- list
- patch
- watch
- apiGroups:
- '*'
resources:
- '*'
verbs:
- '*'
- apiGroups:
- '*'
resources:
Expand Down
64 changes: 62 additions & 2 deletions controller-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,80 @@ sleep 15
echo -e "${GREEN}Check there are no example-nginx pods${NOCOLOR}"
kubectl -n example-nginx get pods
echo -e "${GREEN}Request example-nginx app (should be 503)${NOCOLOR}"
if curl -s -I -H "Host: aergia.localhost" http://localhost:8090/| grep -q "503 Service Unavailable"; then
if curl -s -I -H "Host: aergia.localhost" http://localhost:8090/| grep -q "503 Service"; then
sleep 15
echo -e "${GREEN}Check there are 3 example-nginx pods${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
echo -e "${GREEN}Tear down aergia cluster${NOCOLOR}"
echo -e "${GREEN}Unidled${NOCOLOR}"
else
echo -e "${RED}Curl did not return 200${NOCOLOR}"
tear_down
exit 1
fi
else
echo -e "${RED}Curl did not return 503${NOCOLOR}"
tear_down
exit 1
fi

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"}}}'
sleep 15
echo -e "${GREEN}Check there are 0 example-nginx pods${NOCOLOR}"
kubectl -n example-nginx get pods
echo -e "${GREEN}Request example-nginx app (should be 503)${NOCOLOR}"
if curl -s -I -H "Host: aergia.localhost" http://localhost:8090/| grep -q "503 Service"; then
sleep 15
echo -e "${GREEN}Check there are 3 example-nginx pods${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
echo -e "${GREEN}Unidled${NOCOLOR}"
else
echo -e "${RED}Curl did not return 200${NOCOLOR}"
tear_down
exit 1
fi
else
echo -e "${RED}Curl did not return 503${NOCOLOR}"
tear_down
exit 1
fi
else
echo -e "${RED}Curl did not return 503${NOCOLOR}"
tear_down
exit 1
fi

echo -e "${GREEN}Check that an idled environment can be unidled by label${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"}}}'
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"}}}'
sleep 15
echo -e "${GREEN}Check there are 3 example-nginx pods${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
echo -e "${GREEN}Unidled${NOCOLOR}"
else
echo -e "${RED}Curl did not return 200${NOCOLOR}"
tear_down
exit 1
fi
else
echo -e "${RED}Curl did not return 503${NOCOLOR}"
tear_down
exit 1
fi

tear_down
122 changes: 122 additions & 0 deletions controllers/idling_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
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 controllers

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

"github.com/amazeeio/aergia-controller/handlers/idler"
"github.com/amazeeio/aergia-controller/handlers/unidler"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
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"
)

// IdlingReconciler reconciles idling
type IdlingReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Idler *idler.Idler
Unidler *unidler.Unidler
}

// all the things
// +kubebuilder:rbac:groups=*,resources=*,verbs=*

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

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

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/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,
},
},
})
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))
}
return ctrl.Result{}, nil
}

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/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,
},
},
})
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))
}
return ctrl.Result{}, nil
}

if val, ok := namespace.ObjectMeta.Labels["idling.amazee.io/unidle"]; ok && val == "true" {
opLog.Info(fmt.Sprintf("Unidling environment %s", namespace.Name))
r.Unidler.UnIdle(ctx, namespace.Name, opLog)
nsMergePatch, _ := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]*string{
"idling.amazee.io/unidle": nil,
},
},
})
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))
}
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 *IdlingReconciler) 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
}
31 changes: 31 additions & 0 deletions controllers/predicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package controllers

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 we only watch for create events at this stage.
func (NamespacePredicates) Create(e event.CreateEvent) bool {
return true
}

// Delete returns false if a delete event.
func (NamespacePredicates) Delete(e event.DeleteEvent) bool {
return true
}

// Update returns false if a delete event.
func (NamespacePredicates) Update(e event.UpdateEvent) bool {
return true
}

// Generic returns false if a delete event.
func (NamespacePredicates) Generic(e event.GenericEvent) bool {
return true
}
Loading

0 comments on commit 0000b0b

Please sign in to comment.