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: expose idleReplicaCount param to httpScaledObject #594

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions config/crd/bases/http.keda.sh_httpscaledobjects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ spec:
- jsonPath: .spec.replicas.max
name: MaxReplicas
type: integer
- jsonPath: .spec.replicas.idle
name: IdleReplicas
type: integer
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
Expand Down Expand Up @@ -66,6 +69,11 @@ spec:
replicas:
description: (optional) Replica information
properties:
idle:
description: Amount of replicas to have in the deployment while
being idle
format: int32
type: integer
max:
description: Maximum amount of replicas to have in the deployment
(Default 100)
Expand Down
28 changes: 24 additions & 4 deletions e2e/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"

"github.com/codeskyblue/go-sh"
kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
"github.com/pkg/errors"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -33,7 +32,28 @@ func deleteNS(ns string) error {
return sh.Command("kubectl", "delete", "namespace", ns).Run()
}

func getScaledObject(ctx context.Context, cl client.Client, ns string, name string) error {
var scaledObject kedav1alpha1.ScaledObject
return cl.Get(ctx, k8s.ObjKey(ns, name), &scaledObject)
func getScaledObject(
ctx context.Context,
cl client.Client,
ns,
name string,
) error {
scaledObject, err := k8s.NewScaledObject(
ns,
name,
"",
"",
"",
1,
2,
0,
30,
)
if err != nil {
return err
}
if err := cl.Get(ctx, k8s.ObjKey(ns, name), scaledObject); err != nil {
return err
}
return nil
}
1 change: 1 addition & 0 deletions examples/xkcd/templates/httpscaledobject.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ spec:
replicas:
min: {{ .Values.autoscaling.http.minReplicas }}
max: {{ .Values.autoscaling.http.maxReplicas }}
idle: {{ .Values.autoscaling.http.idleReplicas }}
1 change: 1 addition & 0 deletions examples/xkcd/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ autoscaling:
http:
minReplicas: 0
maxReplicas: 10
idleReplicas: 0
5 changes: 4 additions & 1 deletion operator/api/v1alpha1/httpscaledobject_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ type ReplicaStruct struct {
// Minimum amount of replicas to have in the deployment (Default 0)
Min *int32 `json:"min,omitempty" description:"Minimum amount of replicas to have in the deployment (Default 0)"`
// Maximum amount of replicas to have in the deployment (Default 100)
Max *int32 `json:"max,omitempty" description:"Maximum amount of replicas to have in the deployment (Default 100)"`
Max int32 `json:"max,omitempty" description:"Maximum amount of replicas to have in the deployment (Default 100)"`
// Amount of replicas to have in the deployment while being idle
Idle int32 `json:"idle,omitempty" description:"Amount of replicas to have in the deployment while being idle"`
}

// HTTPScaledObjectSpec defines the desired state of HTTPScaledObject
Expand Down Expand Up @@ -133,6 +135,7 @@ type HTTPScaledObjectStatus struct {
// +kubebuilder:printcolumn:name="ScaleTargetPort",type="integer",JSONPath=".spec.scaleTargetRef"
// +kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.replicas.min"
// +kubebuilder:printcolumn:name="MaxReplicas",type="integer",JSONPath=".spec.replicas.max"
// +kubebuilder:printcolumn:name="IdleReplicas",type="integer",JSONPath=".spec.replicas.idle"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"HTTPScaledObjectIsReady\")].status"

Expand Down
16 changes: 5 additions & 11 deletions operator/controllers/scaled_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,16 @@ func createOrUpdateScaledObject(
httpso *v1alpha1.HTTPScaledObject,
) error {
logger.Info("Creating scaled objects", "external scaler host name", externalScalerHostName)

var minReplicaCount *int32
var maxReplicaCount *int32
if replicas := httpso.Spec.Replicas; replicas != nil {
minReplicaCount = replicas.Min
maxReplicaCount = replicas.Max
}

appScaledObject := k8s.NewScaledObject(
logger.Info("This is the current httpso object", "output", httpso.Spec)
appScaledObject, appErr := k8s.NewScaledObject(
httpso.GetNamespace(),
fmt.Sprintf("%s-app", httpso.GetName()), // HTTPScaledObject name is the same as the ScaledObject name
httpso.Spec.ScaleTargetRef.Deployment,
externalScalerHostName,
httpso.Spec.Host,
minReplicaCount,
maxReplicaCount,
httpso.Spec.Replicas.Min,
httpso.Spec.Replicas.Max,
httpso.Spec.Replicas.Idle,
httpso.Spec.CooldownPeriod,
)

Expand Down
37 changes: 19 additions & 18 deletions operator/controllers/scaled_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,29 @@ func TestCreateOrUpdateScaledObject(t *testing.T) {
metadata.Name,
)

var minReplicaCount *int32
var maxReplicaCount *int32
if replicas := testInfra.httpso.Spec.Replicas; replicas != nil {
minReplicaCount = replicas.Min
maxReplicaCount = replicas.Max
}

r.EqualValues(
minReplicaCount,
spec.MinReplicaCount,
// HTTPScaledObject min/max/idle replicas are int32s,
// but the ScaledObject's spec is decoded into
// an *unsructured.Unstructured (basically a map[string]interface{})
// which is an int64. we need to convert the
// HTTPScaledObject's values into int64s before we compare
r.Equal(
int64(testInfra.httpso.Spec.Replicas.Min),
spec["minReplicaCount"],
)
r.EqualValues(
maxReplicaCount,
spec.MaxReplicaCount,
)
r.Equal(
int64(testInfra.httpso.Spec.Replicas.Idle),
spec["idleReplicaCount"],
)

// now update the min and max replicas on the httpso
// and call createOrUpdateScaledObject again
if spec := &testInfra.httpso.Spec; spec.Replicas == nil {
spec.Replicas = &v1alpha1.ReplicaStruct{
Min: new(int32),
Max: new(int32),
}
}
*testInfra.httpso.Spec.Replicas.Min++
*testInfra.httpso.Spec.Replicas.Max++
testInfra.httpso.Spec.Replicas.Min++
testInfra.httpso.Spec.Replicas.Max++
testInfra.httpso.Spec.Replicas.Idle++
r.NoError(createOrUpdateScaledObject(
testInfra.ctx,
testInfra.cl,
Expand All @@ -107,6 +104,10 @@ func TestCreateOrUpdateScaledObject(t *testing.T) {
*testInfra.httpso.Spec.Replicas.Max,
*spec.MaxReplicaCount,
)
r.Equal(
int64(testInfra.httpso.Spec.Replicas.Idle),
spec["idleReplicaCount"],
)
}

func getSO(
Expand Down
6 changes: 6 additions & 0 deletions operator/controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/kedacore/http-add-on/operator/api/v1alpha1"
httpv1alpha1 "github.com/kedacore/http-add-on/operator/api/v1alpha1"
// +kubebuilder:scaffold:imports
)
Expand Down Expand Up @@ -85,6 +86,11 @@ func newCommonTestInfra(namespace, appName string) *commonTestInfra {
Service: appName,
Port: 8081,
},
Replicas: v1alpha1.ReplicaStruct{
Min: 0,
Max: 20,
Idle: 0,
},
},
}

Expand Down
92 changes: 51 additions & 41 deletions pkg/k8s/scaledobject.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package k8s

import (
kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"bytes"
"text/template"

"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

const (
Expand All @@ -22,42 +23,51 @@ func NewScaledObject(
deploymentName string,
scalerAddress string,
host string,
minReplicas *int32,
maxReplicas *int32,
cooldownPeriod *int32,
) *kedav1alpha1.ScaledObject {
return &kedav1alpha1.ScaledObject{
TypeMeta: metav1.TypeMeta{
APIVersion: kedav1alpha1.SchemeGroupVersion.Identifier(),
Kind: ObjectKind(&kedav1alpha1.ScaledObject{}),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
Labels: Labels(name),
},
Spec: kedav1alpha1.ScaledObjectSpec{
ScaleTargetRef: &kedav1alpha1.ScaleTarget{
APIVersion: appsv1.SchemeGroupVersion.Identifier(),
Kind: ObjectKind(&appsv1.Deployment{}),
Name: deploymentName,
},
PollingInterval: pointer.Int32(soPollingInterval),
CooldownPeriod: cooldownPeriod,
MinReplicaCount: minReplicas,
MaxReplicaCount: maxReplicas,
Advanced: &kedav1alpha1.AdvancedConfig{
RestoreToOriginalReplicaCount: true,
},
Triggers: []kedav1alpha1.ScaleTriggers{
{
Type: soTriggerType,
Metadata: map[string]string{
mkScalerAddress: scalerAddress,
mkHost: host,
},
},
},
},
minReplicas,
maxReplicas int32,
idleReplicas int32,
cooldownPeriod int32,
) (*unstructured.Unstructured, error) {
// https://keda.sh/docs/1.5/faq/
// https://github.com/kedacore/keda/blob/aa0ea79450a1c7549133aab46f5b916efa2364ab/api/v1alpha1/scaledobject_types.go
//
// unstructured.Unstructured only supports specific types in it. see here for the list:
// https://github.com/kubernetes/apimachinery/blob/v0.17.12/pkg/runtime/converter.go#L449-L476
typedLabels := Labels(name)
labels := map[string]interface{}{}
for k, v := range typedLabels {
var vIface interface{} = v
labels[k] = vIface
}

tpl, err := template.ParseFS(scaledObjectTemplateFS, "templates/scaledobject.yaml")
if err != nil {
return nil, err
}

var scaledObjectTemplateBuffer bytes.Buffer
if tplErr := tpl.Execute(&scaledObjectTemplateBuffer, map[string]interface{}{
"Name": name,
"Namespace": namespace,
"Labels": labels,
"MinReplicas": minReplicas,
"MaxReplicas": maxReplicas,
"IdleReplicas": idleReplicas,
"DeploymentName": deploymentName,
"ScalerAddress": scalerAddress,
"Host": host,
"CooldownPeriod": cooldownPeriod,
}); tplErr != nil {
return nil, tplErr
}

var decodedYaml map[string]interface{}
decodeErr := yaml.Unmarshal(scaledObjectTemplateBuffer.Bytes(), &decodedYaml)
if decodeErr != nil {
return nil, decodeErr
}

return &unstructured.Unstructured{
Object: decodedYaml,
}, nil
}
25 changes: 25 additions & 0 deletions pkg/k8s/templates/scaledobject.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: {{ .Name }}
namespace: {{ .Namespace }}
labels:
{{- range $key, $val := .Labels }}
{{ $key }}: {{ $val }}
{{- end }}
spec:
minReplicaCount: {{ .MinReplicas }}
maxReplicaCount: {{ .MaxReplicas }}
idleReplicaCount: {{ .IdleReplicas }}
cooldownPeriod: {{ .CooldownPeriod }}
pollingInterval: 1
scaleTargetRef:
name: {{ .DeploymentName }}
kind: Deployment
triggers:
- type: external-push
metadata:
scalerAddress: {{ .ScalerAddress }}
host: {{ .Host }}
advanced:
restoreToOriginalReplicaCount: true