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: Add multi-namespace test for scaling #75

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -22,6 +22,11 @@ TEST_SECRETS = \

export TEST_FUNCTIONS TEST_SECRETS

.PHONY: lint
lint: ## Verifies `golangci-lint` passes
@echo "+ $@"
@golangci-lint run ./...


clean-kubernetes:
- ./contrib/clean_kubernetes.sh
15 changes: 4 additions & 11 deletions tests/function_helpers_test.go
Original file line number Diff line number Diff line change
@@ -3,9 +3,7 @@ package tests
import (
"context"
"fmt"
"net/http"
"os"
"path"
"testing"

sdk "github.com/openfaas/faas-cli/proxy"
@@ -68,17 +66,12 @@ func deleteFunction(t *testing.T, function *sdk.DeployFunctionSpec) {
}
}

func scaleFunction(t *testing.T, name string, count int) {
func scaleFunction(t *testing.T, name, namespace string, count uint64) {
t.Helper()

// the CLI sdk does not currently support manually scaling
gwURL := resourceURL(t, path.Join("system", "scale-function", name), "")
payload := makeReader(map[string]interface{}{"service": name, "replicas": count})

// TODO : enable auth
_, res := request(t, gwURL, http.MethodPost, config.Auth, payload)
if res.StatusCode != http.StatusAccepted && res.StatusCode != http.StatusOK {
t.Fatalf("scale got %d, wanted %d (or %d)", res.StatusCode, http.StatusAccepted, http.StatusOK)
err := config.Client.ScaleFunction(context.Background(), name, namespace, count)
if err != nil {
t.Fatalf("scale got %s", err)
}
}

7 changes: 0 additions & 7 deletions tests/http_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package tests

import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
@@ -28,11 +26,6 @@ func resourceURL(t *testing.T, reqPath, query string) string {
return uri.String()
}

func makeReader(input interface{}) *bytes.Buffer {
res, _ := json.Marshal(input)
return bytes.NewBuffer(res)
}

func request(t *testing.T, url, method string, auth sdk.ClientAuth, reader io.Reader) ([]byte, *http.Response) {
t.Helper()
return requestContext(t, context.Background(), url, method, auth, reader)
13 changes: 13 additions & 0 deletions tests/main_test.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
@@ -108,6 +109,8 @@ type Config struct {
// AuthEnabled
AuthEnabled bool

IdlerEnabled bool

// SecretUpdate enables/disables the secret update test
SecretUpdate bool
// ScaleToZero enables/disables the scale from zero test
@@ -148,4 +151,14 @@ func FromEnv(config *Config) {
} else {
config.DefaultNamespace = "openfaas-fn"
}

idlerEnabled, ok := os.LookupEnv("idler_enabled")
if ok && idlerEnabled != "" {
enableTest, err := strconv.ParseBool(idlerEnabled)
if err != nil {
log.Fatalf("can parse idler_enabled env flag: %s", err.Error())
}

config.IdlerEnabled = enableTest
}
}
396 changes: 155 additions & 241 deletions tests/scaling_test.go
Original file line number Diff line number Diff line change
@@ -4,258 +4,172 @@ import (
"bytes"
"fmt"
"net/http"
"os"
"path"
"strconv"
"testing"
"time"

sdk "github.com/openfaas/faas-cli/proxy"
"github.com/rakyll/hey/requester"
)

func Test_ScaleMinimum(t *testing.T) {
functionName := "test-min-scale"
minReplicas := uint64(2)
labels := map[string]string{
"com.openfaas.scale.min": fmt.Sprintf("%d", minReplicas),
}
functionRequest := &sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: functionName,
Network: "func_functions",
FProcess: "sha512sum",
Labels: labels,
Namespace: config.DefaultNamespace,
}

deployStatus := deploy(t, functionRequest)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, functionRequest)

fnc := get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != minReplicas {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, minReplicas)
}
}

func Test_ScaleFromZeroDuringInvoke(t *testing.T) {
if config.ScaleToZero {
t.Skip("scale to zero currently returns 500 in faas-swarm")
}
functionName := "test-scale-from-zero"
functionRequest := &sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: functionName,
Network: "func_functions",
FProcess: "sha512sum",
Namespace: config.DefaultNamespace,
}

deployStatus := deploy(t, functionRequest)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, functionRequest)

scaleFunction(t, functionName, 0)

fnc := get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != 0 {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, 0)
}

// this will fail or pass the test
_ = invoke(t, functionRequest, "", "", http.StatusOK)
}

func Test_ScaleUpAndDownFromThroughPut(t *testing.T) {
functionName := "test-throughput-scaling"
minReplicas := uint64(1)
maxReplicas := uint64(2)
labels := map[string]string{
"com.openfaas.scale.min": fmt.Sprintf("%d", minReplicas),
"com.openfaas.scale.max": fmt.Sprintf("%d", maxReplicas),
}
functionRequest := &sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: functionName,
Network: "func_functions",
FProcess: "sha512sum",
Labels: labels,
Namespace: config.DefaultNamespace,
}

deployStatus := deploy(t, functionRequest)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, functionRequest)

functionURL := resourceURL(t, path.Join("function", functionName), "")
req, err := http.NewRequest(http.MethodPost, functionURL, nil)
if err != nil {
t.Fatalf("error with request %s ", err)
}

var loadOutput bytes.Buffer
attempts := 1000
functionLoad := requester.Work{
Request: req,
N: attempts,
Timeout: 10,
C: 2,
QPS: 5.0,
DisableKeepAlives: true,
Writer: &loadOutput,
}

functionLoad.Init()
functionLoad.Run()

fnc := get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != maxReplicas {
t.Logf("function load output %s", loadOutput.String())
t.Fatalf("never reached max scale %d, only %d replicas after %d attempts", maxReplicas, fnc.Replicas, attempts)
}

// cooldown
time.Sleep(time.Minute)
fnc = get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != minReplicas {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, minReplicas)
}
type scalingTestCase struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really Nice refactoring !!

name string
spec sdk.DeployFunctionSpec
minReplicas int
maxReplicas int
targetReplicas int
withLoad bool
scaleFromZero bool
}

func Test_ScalingDisabledViaLabels(t *testing.T) {
functionName := "test-scaling-disabled"
minReplicas := uint64(2)
maxReplicas := minReplicas
// Per the docs, setting these values equal to each other will disabled
// scaling
// https://docs.openfaas.com/architecture/autoscaling/#minmax-replicas
labels := map[string]string{
"com.openfaas.scale.min": fmt.Sprintf("%d", minReplicas),
"com.openfaas.scale.max": fmt.Sprintf("%d", maxReplicas),
}
functionRequest := &sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: functionName,
Network: "func_functions",
FProcess: "sha512sum",
Labels: labels,
Namespace: config.DefaultNamespace,
}

deployStatus := deploy(t, functionRequest)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, functionRequest)

functionURL := resourceURL(t, path.Join("function", functionName), "")
req, err := http.NewRequest(http.MethodPost, functionURL, nil)
if err != nil {
t.Fatalf("error with request %s ", err)
}

var loadOutput bytes.Buffer
attempts := 1000
functionLoad := requester.Work{
Request: req,
N: attempts,
Timeout: 10,
C: 2,
QPS: 5.0,
DisableKeepAlives: true,
Writer: &loadOutput,
}

functionLoad.Init()
functionLoad.Run()

fnc := get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != minReplicas {
t.Logf("function load output %s", loadOutput.String())
t.Fatalf("unexpected scaling, expected %d, got %d replicas after %d attempts", minReplicas, fnc.Replicas, attempts)
}
func (t *scalingTestCase) SetNamespace(namespace string) {
t.name = fmt.Sprintf("%s in %s", t.name, namespace)
t.spec.Namespace = namespace
}

func Test_ScaleToZero(t *testing.T) {

idlerEnabled := os.Getenv("idler_enabled")
if idlerEnabled == "" {
idlerEnabled = "false"
}

enableTest, err := strconv.ParseBool(idlerEnabled)
if err != nil {
t.Fatal(err)
}

if !enableTest {
t.Skip("set 'idler_enabled' to test scale to zero")
}

functionName := "test-scaling-to-zero"
maxReplicas := uint64(2)
labels := map[string]string{
"com.openfaas.scale.max": fmt.Sprintf("%d", maxReplicas),
"com.openfaas.scale.zero": "true",
}
functionRequest := &sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: functionName,
Network: "func_functions",
FProcess: "sha512sum",
Labels: labels,
Namespace: config.DefaultNamespace,
}

deployStatus := deploy(t, functionRequest)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, functionRequest)

functionURL := resourceURL(t, path.Join("function", functionName), "")
req, err := http.NewRequest(http.MethodPost, functionURL, nil)
if err != nil {
t.Fatalf("error with request %s ", err)
}

var loadOutput bytes.Buffer
attempts := 1000
functionLoad := requester.Work{
Request: req,
N: attempts,
Timeout: 10,
C: 2,
QPS: 5.0,
DisableKeepAlives: true,
Writer: &loadOutput,
}

functionLoad.Init()
functionLoad.Run()

fnc := get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != maxReplicas {
t.Logf("function load output %s", loadOutput.String())
t.Fatalf("never reached max scale %d, only %d replicas after %d attempts", maxReplicas, fnc.Replicas, attempts)
}

// cooldown
time.Sleep(2 * time.Minute)
fnc = get(t, functionName, config.DefaultNamespace)
if fnc.Replicas != 0 {
t.Fatalf("got %d replicas, wanted 0", fnc.Replicas)
func Test_Scaling(t *testing.T) {
cases := []scalingTestCase{
{
name: "deploy with non-default minimum replicas",
spec: sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: "test-min-scale",
Network: "func_functions",
FProcess: "sha512sum",
Labels: map[string]string{
"com.openfaas.scale.min": fmt.Sprintf("%d", uint64(2)),
},
Namespace: config.DefaultNamespace,
},
minReplicas: 2,
},
{
name: "scale up from zero replicas after invoke",
spec: sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: "test-scale-from-zero",
Network: "func_functions",
FProcess: "sha512sum",
Namespace: config.DefaultNamespace,
},
scaleFromZero: true,
targetReplicas: 1,
minReplicas: 1,
},
{
name: "scale up and down via load monitoring",
spec: sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: "test-throughput-scaling",
Network: "func_functions",
FProcess: "sha512sum",
Labels: map[string]string{
"com.openfaas.scale.min": fmt.Sprintf("%d", 1),
"com.openfaas.scale.max": fmt.Sprintf("%d", 2),
},
Namespace: config.DefaultNamespace,
},
minReplicas: 1,
maxReplicas: 2,
targetReplicas: 1,
withLoad: true,
},
{
name: "scale to zero",
spec: sdk.DeployFunctionSpec{
Image: "functions/alpine:latest",
FunctionName: "test-scaling-to-zero",
Network: "func_functions",
FProcess: "sha512sum",
Labels: map[string]string{
"com.openfaas.scale.max": fmt.Sprintf("%d", 2),
"com.openfaas.scale.zero": "true",
},
Namespace: config.DefaultNamespace,
},
minReplicas: 1,
maxReplicas: 2,
targetReplicas: 0,
},
}
if len(config.Namespaces) > 0 {
defaultCasesLen := len(cases)
for index := 0; index < defaultCasesLen; index++ {
namespacedCase := cases[index]
namespacedCase.SetNamespace(config.Namespaces[0])
cases = append(cases, namespacedCase)
}
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {

if !config.IdlerEnabled && tc.spec.Labels["com.openfaas.scale.zero"] == "true" {
t.Skip("set 'idler_enabled' to test scale to zero")
return
}

deployStatus := deploy(t, &tc.spec)
if deployStatus != http.StatusOK && deployStatus != http.StatusAccepted {
t.Fatalf("got %d, wanted %d or %d", deployStatus, http.StatusOK, http.StatusAccepted)
}

defer deleteFunction(t, &tc.spec)

time.Sleep(5 * time.Second)
fnc := get(t, tc.spec.FunctionName, tc.spec.Namespace)
if fnc.Replicas != uint64(tc.minReplicas) {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, tc.minReplicas)
}

if tc.scaleFromZero {
scaleFunction(t, tc.spec.FunctionName, tc.spec.Namespace, 0)

fnc := get(t, tc.spec.FunctionName, tc.spec.Namespace)
if fnc.Replicas != 0 {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, 0)
}

// this will fail or pass the test
_ = invoke(t, &tc.spec, "", "", http.StatusOK)
}

if tc.withLoad {
functionURL := resourceURL(t, path.Join("function", tc.spec.FunctionName+"."+tc.spec.Namespace), "")
req, err := http.NewRequest(http.MethodPost, functionURL, nil)
if err != nil {
t.Fatalf("error with request %s ", err)
}

var loadOutput bytes.Buffer
attempts := 1000
functionLoad := requester.Work{
Request: req,
N: attempts,
Timeout: 10,
C: 2,
QPS: 5.0,
DisableKeepAlives: true,
Writer: &loadOutput,
}

functionLoad.Run()

fnc := get(t, tc.spec.FunctionName, tc.spec.Namespace)
if fnc.Replicas != uint64(tc.maxReplicas) {
t.Logf("function load output %s", loadOutput.String())
t.Fatalf("never reached max scale %d, only %d replicas after %d attempts", tc.maxReplicas, fnc.Replicas, attempts)
}

// no need to test cooldown if min=max, because it is effectively tested above
if tc.maxReplicas > tc.minReplicas {
time.Sleep(time.Minute)
fnc = get(t, tc.spec.FunctionName, tc.spec.Namespace)
if fnc.Replicas != uint64(tc.targetReplicas) {
t.Fatalf("got %d replicas, wanted %d", fnc.Replicas, tc.targetReplicas)
}
}
}
})
}
}