Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support the new idle labels and annotations #459

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
}
}
Loading