Skip to content

Commit

Permalink
Merge pull request #459 from uselagoon/idling-annotations
Browse files Browse the repository at this point in the history
feat: support the new idle labels and annotations
  • Loading branch information
smlx authored Oct 4, 2024
2 parents 442e1da + 9a376b8 commit c0c0e1b
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 25 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ require (
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand All @@ -77,6 +78,7 @@ require (
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -226,6 +228,8 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
2 changes: 1 addition & 1 deletion internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var timeoutSeconds = int64(timeout / time.Second)
// Client is a k8s client.
type Client struct {
config *rest.Config
clientset *kubernetes.Clientset
clientset kubernetes.Interface
logStreamIDs sync.Map
}

Expand Down
81 changes: 59 additions & 22 deletions internal/k8s/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,21 @@ import (
"k8s.io/client-go/tools/remotecommand"
)

const (
idleAnnotation = "idling.amazee.io/unidle-replicas"
var (
// idleReplicaAnnotations are used to determine how many replicas to set when
// scaling up a deployment from idle. The annotations are in priority order
// from high to low. The first annotation found on a deployment will be used.
idleReplicaAnnotations = []string{
"idling.lagoon.sh/unidle-replicas",
"idling.amazee.io/unidle-replicas",
}
// idleWatchLabels are used to select deployments to scale when unidling a
// namespace. The labels are in priority order from high to low. The first
// label found on any deployment will be used.
idleWatchLabels = []string{
"idling.lagoon.sh/watch=true",
"idling.amazee.io/watch=true",
}
)

// podContainer returns the first pod and first container inside that pod for
Expand Down Expand Up @@ -68,33 +81,57 @@ func (c *Client) hasRunningPod(ctx context.Context,
}
}

// unidleReplicas checks the unidle-replicas annotation for the number of
// replicas to restore. If the label cannot be read or parsed, 1 is returned.
// The return value is clamped to the interval [1,16].
// unidleReplicas checks the idleReplicaAnnotations for the number of replicas
// to restore. If the labels cannot be found or parsed, 1 is returned. The
// return value is clamped to the interval [1,16].
func unidleReplicas(deploy appsv1.Deployment) int {
rs, ok := deploy.Annotations[idleAnnotation]
if !ok {
return 1
}
r, err := strconv.Atoi(rs)
if err != nil || r < 1 {
return 1
for _, ra := range idleReplicaAnnotations {
rs, ok := deploy.Annotations[ra]
if !ok {
continue
}
r, err := strconv.Atoi(rs)
if err != nil || r < 1 {
return 1
}
if r > 16 {
return 16
}
return r
}
if r > 16 {
return 16
return 1
}

// idledDeploys returns the DeploymentList of idled deployments in the given
// namespace.
func (c *Client) idledDeploys(ctx context.Context, namespace string) (
*appsv1.DeploymentList, error,
) {
var deploys *appsv1.DeploymentList
for _, selector := range idleWatchLabels {
deploys, err := c.clientset.AppsV1().Deployments(namespace).List(ctx,
metav1.ListOptions{
LabelSelector: selector,
})
if err != nil {
return nil, fmt.Errorf("couldn't select deploys by label: %v", err)
}
if deploys != nil && len(deploys.Items) > 0 {
return deploys, nil
}
}
return r
return deploys, nil
}

// unidleNamespace scales all deployments with the
// "idling.amazee.io/watch=true" label up to the number of replicas in the
// "idling.amazee.io/unidle-replicas" label.
// unidleNamespace scales all deployments with the idleWatchLabels up to the
// number of replicas in the idleReplicaAnnotations.
func (c *Client) unidleNamespace(ctx context.Context, namespace string) error {
deploys, err := c.clientset.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{
LabelSelector: "idling.amazee.io/watch=true",
})
deploys, err := c.idledDeploys(ctx, namespace)
if err != nil {
return fmt.Errorf("couldn't select deploys by label: %v", err)
return fmt.Errorf("couldn't get idled deploys: %v", err)
}
if deploys == nil {
return nil // no deploys to unidle
}
for _, deploy := range deploys.Items {
// check if idled
Expand Down
130 changes: 128 additions & 2 deletions internal/k8s/exec_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package k8s

import (
"context"
"testing"

"github.com/alecthomas/assert/v2"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestUnidleReplicas(t *testing.T) {
func TestUnidleReplicasParsing(t *testing.T) {
var testCases = map[string]struct {
input string
expect int
Expand All @@ -28,10 +30,134 @@ func TestUnidleReplicas(t *testing.T) {
t.Run(name, func(tt *testing.T) {
deploy := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{idleAnnotation: tc.input},
Annotations: map[string]string{idleReplicaAnnotations[0]: tc.input},
},
}
assert.Equal(tt, tc.expect, unidleReplicas(deploy), name)
})
}
}

func TestUnidleReplicasLabels(t *testing.T) {
for _, ra := range idleReplicaAnnotations {
t.Run(ra, func(tt *testing.T) {
deploy := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{ra: "9"},
},
}
assert.Equal(tt, 9, unidleReplicas(deploy), ra)
})
}
}

func deployNames(deploys *appsv1.DeploymentList) []string {
var names []string
if deploys == nil {
return names // no deploys to unidle
}
for _, deploy := range deploys.Items {
names = append(names, deploy.Name)
}
return names
}

func TestIdledDeployLabels(t *testing.T) {
testNS := "testns"
var testCases = map[string]struct {
deploys *appsv1.DeploymentList
expect []string
}{
"prefer lagoon.sh": {
deploys: &appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "one",
Namespace: testNS,
Labels: map[string]string{
"idling.lagoon.sh/watch": "true",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "two",
Namespace: testNS,
Labels: map[string]string{
"idling.amazee.io/watch": "true",
},
},
},
},
},
expect: []string{"one"},
},
"fall back to amazee.io": {
deploys: &appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "one",
Namespace: testNS,
Labels: map[string]string{
"idling.amazee.io/watch": "true",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "two",
Namespace: testNS,
Labels: map[string]string{
"idling.amazee.io/watch": "true",
},
},
},
},
},
expect: []string{"one", "two"},
},
"ignore mislabelled deploys": {
deploys: &appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "one",
Namespace: testNS,
Labels: map[string]string{
"idling.foo/watch": "true",
},
},
},
},
},
},
"ignore other namespaces": {
deploys: &appsv1.DeploymentList{
Items: []appsv1.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "one",
Namespace: "wrongns",
Labels: map[string]string{
"idling.lagoon.sh/watch": "true",
},
},
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
// create fake Kubernetes client with test deploys
c := &Client{
clientset: fake.NewSimpleClientset(tc.deploys),
}
deploys, err := c.idledDeploys(context.Background(), testNS)
assert.NoError(tt, err, name)
assert.Equal(tt, tc.expect, deployNames(deploys), name)
})
}
}

0 comments on commit c0c0e1b

Please sign in to comment.