Files
fission-fission/pkg/executor/util/deployment_test.go
T

242 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: The Fission Authors
//
// SPDX-License-Identifier: Apache-2.0
package util
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
fv1 "github.com/fission/fission/pkg/apis/core/v1"
"github.com/fission/fission/pkg/utils/loggerfactory"
)
func secret(ns, name, rv string) *apiv1.Secret {
return &apiv1.Secret{Namespace: ns, Name: name, ResourceVersion: rv}
}
func configMap(ns, name, rv string) *apiv1.ConfigMap {
return &apiv1.ConfigMap{Namespace: ns, Name: name, ResourceVersion: rv}
}
func secretRef(ns, name string) fv1.SecretReference {
return fv1.SecretReference{Namespace: ns, Name: name}
}
func configMapRef(ns, name string) fv1.ConfigMapReference {
return fv1.ConfigMapReference{Namespace: ns, Name: name}
}
func TestReferencedResourcesRVSum(t *testing.T) {
t.Parallel()
const ns = "default"
tests := []struct {
name string
objects []runtime.Object
spec fv1.FunctionSpec
want int64
}{
{
name: "sums referenced secret and configmap resource versions",
objects: []runtime.Object{secret(ns, "s1", "10"), configMap(ns, "c1", "20")},
spec: fv1.FunctionSpec{
Secrets: []fv1.SecretReference{secretRef(ns, "s1")},
ConfigMaps: []fv1.ConfigMapReference{configMapRef(ns, "c1")},
},
want: 30,
},
{
name: "ignores unreferenced objects in the namespace",
objects: []runtime.Object{
secret(ns, "s1", "10"), secret(ns, "s2", "99"),
configMap(ns, "c1", "20"), configMap(ns, "c2", "77"),
},
spec: fv1.FunctionSpec{
Secrets: []fv1.SecretReference{secretRef(ns, "s1")},
ConfigMaps: []fv1.ConfigMapReference{configMapRef(ns, "c1")},
},
want: 30,
},
{
name: "empty references return zero without listing",
objects: []runtime.Object{secret(ns, "s1", "10")},
want: 0,
},
{
name: "missing referenced object contributes zero and does not error",
objects: []runtime.Object{secret(ns, "s1", "10")},
spec: fv1.FunctionSpec{
Secrets: []fv1.SecretReference{secretRef(ns, "s1"), secretRef(ns, "ghost")},
},
want: 10,
},
{
name: "cross-namespace reference is skipped",
objects: []runtime.Object{secret("other", "s1", "10")},
spec: fv1.FunctionSpec{
Secrets: []fv1.SecretReference{secretRef("other", "s1")},
},
want: 0,
},
{
name: "env valueFrom references contribute (RFC-0030)",
objects: []runtime.Object{secret(ns, "s1", "10"), configMap(ns, "c1", "20")},
spec: fv1.FunctionSpec{
Env: []apiv1.EnvVar{
{Name: "A", ValueFrom: &apiv1.EnvVarSource{SecretKeyRef: &apiv1.SecretKeySelector{
Name: "s1", Key: "k"}}},
{Name: "B", ValueFrom: &apiv1.EnvVarSource{ConfigMapKeyRef: &apiv1.ConfigMapKeySelector{
Name: "c1", Key: "k"}}},
},
},
want: 30,
},
{
name: "envFrom references contribute (RFC-0030)",
objects: []runtime.Object{secret(ns, "s1", "10"), configMap(ns, "c1", "20")},
spec: fv1.FunctionSpec{
EnvFrom: []apiv1.EnvFromSource{
{SecretRef: &apiv1.SecretEnvSource{Name: "s1"}},
{ConfigMapRef: &apiv1.ConfigMapEnvSource{Name: "c1"}},
},
},
want: 30,
},
{
name: "object referenced via both channels contributes once",
objects: []runtime.Object{secret(ns, "s1", "10")},
spec: fv1.FunctionSpec{
Secrets: []fv1.SecretReference{secretRef(ns, "s1")},
EnvFrom: []apiv1.EnvFromSource{
{SecretRef: &apiv1.SecretEnvSource{Name: "s1"}},
},
},
want: 10,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client := fake.NewClientset(tc.objects...)
got, err := ReferencedResourcesRVSum(t.Context(), client, ns, tc.spec)
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestScaleDeployment(t *testing.T) {
t.Parallel()
const ns, name = "default", "dep1"
replicas := int32(1)
dep := &appsv1.Deployment{
Namespace: ns, Name: name,
Spec: appsv1.DeploymentSpec{Replicas: &replicas},
}
client := fake.NewClientset(dep)
// Capture the scale subresource update directly: the fake clientset's
// GetScale read-back has a known scheme-conversion bug, so we assert on
// the object the helper sends rather than reading it back.
var captured *autoscalingv1.Scale
client.PrependReactor("update", "deployments", func(action k8stesting.Action) (bool, runtime.Object, error) {
if action.GetSubresource() != "scale" {
return false, nil, nil
}
// SAFETY: this reactor is registered for "update" on the scale subresource, so the action is an UpdateAction carrying a *Scale.
obj := action.(k8stesting.UpdateAction).GetObject()
// SAFETY: see above; the update object on the scale subresource is a *Scale.
captured = obj.(*autoscalingv1.Scale)
return true, obj, nil
})
err := ScaleDeployment(t.Context(), client, loggerfactory.GetLogger(), ns, name, 3)
require.NoError(t, err)
require.NotNil(t, captured)
assert.Equal(t, int32(3), captured.Spec.Replicas)
}
func TestWaitForDeployment(t *testing.T) {
t.Parallel()
const ns = "default"
t.Run("returns once available replicas meet the target", func(t *testing.T) {
t.Parallel()
dep := &appsv1.Deployment{
Namespace: ns, Name: "ready",
Status: appsv1.DeploymentStatus{AvailableReplicas: 1},
}
client := fake.NewClientset(dep)
// AvailableReplicas already satisfies the target, so the poll loop
// returns on the first iteration without sleeping. The wall-clock
// timeout path (floored at fv1.DefaultSpecializationTimeOut) is left
// to integration coverage so the unit test stays fast.
got, err := WaitForDeployment(t.Context(), client, loggerfactory.GetLogger(), dep, 1, 1)
require.NoError(t, err)
require.NotNil(t, got)
assert.GreaterOrEqual(t, got.Status.AvailableReplicas, int32(1))
})
t.Run("surfaces a get error", func(t *testing.T) {
t.Parallel()
client := fake.NewClientset()
missing := &appsv1.Deployment{Namespace: ns, Name: "ghost"}
got, err := WaitForDeployment(t.Context(), client, loggerfactory.GetLogger(), missing, 1, 1)
require.Error(t, err)
assert.Nil(t, got)
})
t.Run("returns promptly when the context is cancelled", func(t *testing.T) {
t.Parallel()
// Deployment exists but is not yet available, so the loop would
// otherwise sleep; a cancelled context must short-circuit instead of
// blocking for the (floored) timeout window.
dep := &appsv1.Deployment{
Namespace: ns, Name: "pending",
Status: appsv1.DeploymentStatus{AvailableReplicas: 0},
}
client := fake.NewClientset(dep)
ctx, cancel := context.WithCancel(t.Context())
cancel()
got, err := WaitForDeployment(ctx, client, loggerfactory.GetLogger(), dep, 1, 1)
require.Error(t, err)
assert.Nil(t, got)
})
}
func TestNextWaitPollDelay(t *testing.T) {
t.Parallel()
// ×1.5 growth from the initial delay, capped at the old fixed interval.
assert.Equal(t, 75*time.Millisecond, nextWaitPollDelay(waitPollInitialDelay))
assert.Equal(t, waitPollMaxDelay, nextWaitPollDelay(900*time.Millisecond))
assert.Equal(t, waitPollMaxDelay, nextWaitPollDelay(waitPollMaxDelay))
// The schedule reaches its cap quickly, bounding the extra apiserver GETs
// an adaptive poll adds over the old 1/s: ~9 in the first ~5s of a wait.
d, polls := waitPollInitialDelay, 0
for elapsed := time.Duration(0); d < waitPollMaxDelay; polls++ {
elapsed += d
d = nextWaitPollDelay(d)
require.Less(t, elapsed, 6*time.Second)
}
assert.LessOrEqual(t, polls, 10)
}