mirror of https://github.com/grafana/loki
Provide handler for LokiStack create and update events (#27)
parent
0949632dca
commit
03ce63b730
@ -0,0 +1,440 @@ |
||||
package handlers_test |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"flag" |
||||
"io/ioutil" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/ViaQ/logerr/log" |
||||
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1" |
||||
"github.com/ViaQ/loki-operator/internal/external/k8s/k8sfakes" |
||||
"github.com/ViaQ/loki-operator/internal/handlers" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
corev1 "k8s.io/api/core/v1" |
||||
apierrors "k8s.io/apimachinery/pkg/api/errors" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
"k8s.io/apimachinery/pkg/types" |
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" |
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme" |
||||
"k8s.io/utils/pointer" |
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime" |
||||
"sigs.k8s.io/controller-runtime/pkg/client" |
||||
) |
||||
|
||||
var scheme = runtime.NewScheme() |
||||
|
||||
func TestMain(m *testing.M) { |
||||
testing.Init() |
||||
flag.Parse() |
||||
|
||||
if testing.Verbose() { |
||||
// set to the highest for verbose testing
|
||||
log.SetLogLevel(5) |
||||
} else { |
||||
if err := log.SetOutput(ioutil.Discard); err != nil { |
||||
// This would only happen if the default logger was changed which it hasn't so
|
||||
// we can assume that a panic is necessary and the developer is to blame.
|
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
// Register the clientgo and CRD schemes
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme)) |
||||
utilruntime.Must(lokiv1beta1.AddToScheme(scheme)) |
||||
|
||||
log.Init("testing") |
||||
os.Exit(m.Run()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenGetReturnsNotFound_DoesNotError(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
k.GetStub = func(ctx context.Context, name types.NamespacedName, object client.Object) error { |
||||
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found") |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was NOT called because the Get failed
|
||||
require.Zero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenGetReturnsAnErrorOtherThanNotFound_ReturnsTheError(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
badRequestErr := apierrors.NewBadRequest("you do not belong here") |
||||
k.GetStub = func(ctx context.Context, name types.NamespacedName, object client.Object) error { |
||||
return badRequestErr |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
|
||||
require.Equal(t, badRequestErr, errors.Unwrap(err)) |
||||
|
||||
// make sure create was NOT called because the Get failed
|
||||
require.Zero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_SetsNamespaceOnAllObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, _ client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
return nil |
||||
} |
||||
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found") |
||||
} |
||||
|
||||
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error { |
||||
assert.Equal(t, r.Namespace, o.GetNamespace()) |
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was called
|
||||
require.NotZero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_SetsOwnerRefOnAllObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "LokiStack", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
Namespace: "some-ns", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
// Create looks up the CR first, so we need to return our fake stack
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &stack) |
||||
return nil |
||||
} |
||||
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found") |
||||
} |
||||
|
||||
expected := metav1.OwnerReference{ |
||||
APIVersion: lokiv1beta1.GroupVersion.String(), |
||||
Kind: stack.Kind, |
||||
Name: stack.Name, |
||||
UID: stack.UID, |
||||
Controller: pointer.BoolPtr(true), |
||||
BlockOwnerDeletion: pointer.BoolPtr(true), |
||||
} |
||||
|
||||
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error { |
||||
// OwnerRefs are appended so we have to find ours in the list
|
||||
var ref metav1.OwnerReference |
||||
var found bool |
||||
for _, or := range o.GetOwnerReferences() { |
||||
if or.UID == stack.UID { |
||||
found = true |
||||
ref = or |
||||
break |
||||
} |
||||
} |
||||
|
||||
require.True(t, found, "expected to find a matching ownerRef, but did not") |
||||
require.EqualValues(t, expected, ref) |
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was called
|
||||
require.NotZero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenSetControllerRefInvalid_ContinueWithOtherObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "LokiStack", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
// Set invalid namespace here, because
|
||||
// because cross-namespace controller
|
||||
// references are not allowed
|
||||
Namespace: "invalid-ns", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
// Create looks up the CR first, so we need to return our fake stack
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &stack) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
|
||||
// make sure error is returned to re-trigger reconciliation
|
||||
require.Error(t, err) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenGetReturnsNoError_UpdateObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "LokiStack", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
Namespace: "some-ns", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
svc := corev1.Service{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "Service", |
||||
APIVersion: "v1", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "loki-gossip-ring-my-stack", |
||||
Namespace: "some-ns", |
||||
Labels: map[string]string{ |
||||
"app.kubernetes.io/name": "loki", |
||||
"app.kubernetes.io/provider": "openshift", |
||||
"loki.grafana.com/name": "my-stack", |
||||
|
||||
// Add custom label to fake semantic not equal
|
||||
"test": "test", |
||||
}, |
||||
OwnerReferences: []metav1.OwnerReference{ |
||||
{ |
||||
APIVersion: "loki.openshift.io/v1beta1", |
||||
Kind: "LokiStack", |
||||
Name: "someStack", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
Controller: pointer.BoolPtr(true), |
||||
BlockOwnerDeletion: pointer.BoolPtr(true), |
||||
}, |
||||
}, |
||||
}, |
||||
Spec: corev1.ServiceSpec{ |
||||
ClusterIP: "None", |
||||
Ports: []corev1.ServicePort{ |
||||
{ |
||||
Name: "gossip", |
||||
Port: 7946, |
||||
Protocol: "TCP", |
||||
}, |
||||
}, |
||||
Selector: map[string]string{ |
||||
"app.kubernetes.io/name": "loki", |
||||
"app.kubernetes.io/provider": "openshift", |
||||
"loki.grafana.com/name": "my-stack", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
// Create looks up the CR first, so we need to return our fake stack
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &stack) |
||||
} |
||||
|
||||
if svc.Name == name.Name && svc.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &svc) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create not called
|
||||
require.Zero(t, k.CreateCallCount()) |
||||
|
||||
// make sure update was called
|
||||
require.NotZero(t, k.UpdateCallCount()) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenCreateReturnsError_ContinueWithOtherObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "LokiStack", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
Namespace: "some-ns", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
// GetStub looks up the CR first, so we need to return our fake stack
|
||||
// return NotFound for everything else to trigger create.
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &stack) |
||||
return nil |
||||
} |
||||
return apierrors.NewNotFound(schema.GroupResource{}, "something is not found") |
||||
} |
||||
|
||||
// CreateStub returns an error for each resource to trigger reconciliation a new.
|
||||
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error { |
||||
return apierrors.NewTooManyRequestsError("too many create requests") |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
|
||||
// make sure error is returned to re-trigger reconciliation
|
||||
require.Error(t, err) |
||||
} |
||||
|
||||
func TestCreateOrUpdateLokiStack_WhenUpdateReturnsError_ContinueWithOtherObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "LokiStack", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
Namespace: "some-ns", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
svc := corev1.Service{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "Service", |
||||
APIVersion: "v1", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "loki-gossip-ring-my-stack", |
||||
Namespace: "some-ns", |
||||
Labels: map[string]string{ |
||||
"app.kubernetes.io/name": "loki", |
||||
"app.kubernetes.io/provider": "openshift", |
||||
"loki.grafana.com/name": "my-stack", |
||||
|
||||
// Add custom label to fake semantic not equal
|
||||
"test": "test", |
||||
}, |
||||
OwnerReferences: []metav1.OwnerReference{ |
||||
{ |
||||
APIVersion: "loki.openshift.io/v1beta1", |
||||
Kind: "LokiStack", |
||||
Name: "someStack", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
Controller: pointer.BoolPtr(true), |
||||
BlockOwnerDeletion: pointer.BoolPtr(true), |
||||
}, |
||||
}, |
||||
}, |
||||
Spec: corev1.ServiceSpec{ |
||||
ClusterIP: "None", |
||||
Ports: []corev1.ServicePort{ |
||||
{ |
||||
Name: "gossip", |
||||
Port: 7946, |
||||
Protocol: "TCP", |
||||
}, |
||||
}, |
||||
Selector: map[string]string{ |
||||
"app.kubernetes.io/name": "loki", |
||||
"app.kubernetes.io/provider": "openshift", |
||||
"loki.grafana.com/name": "my-stack", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
// GetStub looks up the CR first, so we need to return our fake stack
|
||||
// return NotFound for everything else to trigger create.
|
||||
k.GetStub = func(_ context.Context, name types.NamespacedName, object client.Object) error { |
||||
if r.Name == name.Name && r.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &stack) |
||||
} |
||||
if svc.Name == name.Name && svc.Namespace == name.Namespace { |
||||
k.SetClientObject(object, &svc) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// CreateStub returns an error for each resource to trigger reconciliation a new.
|
||||
k.UpdateStub = func(_ context.Context, o client.Object, _ ...client.UpdateOption) error { |
||||
return apierrors.NewTooManyRequestsError("too many create requests") |
||||
} |
||||
|
||||
err := handlers.CreateOrUpdateLokiStack(context.TODO(), r, k, scheme) |
||||
|
||||
// make sure error is returned to re-trigger reconciliation
|
||||
require.Error(t, err) |
||||
} |
||||
@ -1,163 +0,0 @@ |
||||
package handlers_test |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"flag" |
||||
"io/ioutil" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/ViaQ/logerr/log" |
||||
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1" |
||||
"github.com/ViaQ/loki-operator/internal/external/k8s/k8sfakes" |
||||
"github.com/ViaQ/loki-operator/internal/handlers" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
apierrors "k8s.io/apimachinery/pkg/api/errors" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
"k8s.io/apimachinery/pkg/types" |
||||
"k8s.io/utils/pointer" |
||||
ctrl "sigs.k8s.io/controller-runtime" |
||||
"sigs.k8s.io/controller-runtime/pkg/client" |
||||
) |
||||
|
||||
func TestMain(m *testing.M) { |
||||
testing.Init() |
||||
flag.Parse() |
||||
|
||||
if testing.Verbose() { |
||||
// set to the highest for verbose testing
|
||||
log.SetLogLevel(5) |
||||
} else { |
||||
if err := log.SetOutput(ioutil.Discard); err != nil { |
||||
// This would only happen if the default logger was changed which it hasn't so
|
||||
// we can assume that a panic is necessary and the developer is to blame.
|
||||
panic(err) |
||||
} |
||||
} |
||||
log.Init("testing") |
||||
os.Exit(m.Run()) |
||||
} |
||||
|
||||
func TestCreateLokiStack_WhenGetReturnsNotFound_DoesNotError(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
k.GetStub = func(ctx context.Context, name types.NamespacedName, object client.Object) error { |
||||
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found") |
||||
} |
||||
|
||||
err := handlers.CreateLokiStack(context.TODO(), r, k) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was NOT called because the Get failed
|
||||
require.Zero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateLokiStack_WhenGetReturnsAnErrorOtherThanNotFound_ReturnsTheError(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
badRequestErr := apierrors.NewBadRequest("you do not belong here") |
||||
k.GetStub = func(ctx context.Context, name types.NamespacedName, object client.Object) error { |
||||
return badRequestErr |
||||
} |
||||
|
||||
err := handlers.CreateLokiStack(context.TODO(), r, k) |
||||
|
||||
require.Equal(t, badRequestErr, errors.Unwrap(err)) |
||||
|
||||
// make sure create was NOT called because the Get failed
|
||||
require.Zero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateLokiStack_SetsNamespaceOnAllObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error { |
||||
assert.Equal(t, r.Namespace, o.GetNamespace()) |
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateLokiStack(context.TODO(), r, k) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was called
|
||||
require.NotZero(t, k.CreateCallCount()) |
||||
} |
||||
|
||||
func TestCreateLokiStack_SetsOwnerRefOnAllObjects(t *testing.T) { |
||||
k := &k8sfakes.FakeClient{} |
||||
r := ctrl.Request{ |
||||
NamespacedName: types.NamespacedName{ |
||||
Name: "my-stack", |
||||
Namespace: "some-ns", |
||||
}, |
||||
} |
||||
|
||||
stack := lokiv1beta1.LokiStack{ |
||||
TypeMeta: metav1.TypeMeta{ |
||||
Kind: "someKind", |
||||
}, |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "someStack", |
||||
Namespace: "someNamespace", |
||||
UID: "b23f9a38-9672-499f-8c29-15ede74d3ece", |
||||
}, |
||||
} |
||||
|
||||
// Create looks up the CR first, so we need to return our fake stack
|
||||
k.GetStub = func(_ context.Context, _ types.NamespacedName, object client.Object) error { |
||||
k.SetClientObject(object, &stack) |
||||
return nil |
||||
} |
||||
|
||||
expected := metav1.OwnerReference{ |
||||
APIVersion: lokiv1beta1.GroupVersion.String(), |
||||
Kind: stack.Kind, |
||||
Name: stack.Name, |
||||
UID: stack.UID, |
||||
Controller: pointer.BoolPtr(true), |
||||
} |
||||
|
||||
k.CreateStub = func(_ context.Context, o client.Object, _ ...client.CreateOption) error { |
||||
// OwnerRefs are appended so we have to find ours in the list
|
||||
var ref metav1.OwnerReference |
||||
var found bool |
||||
for _, or := range o.GetOwnerReferences() { |
||||
if or.UID == stack.UID { |
||||
found = true |
||||
ref = or |
||||
break |
||||
} |
||||
} |
||||
|
||||
require.True(t, found, "expected to find a matching ownerRef, but did not") |
||||
require.EqualValues(t, expected, ref) |
||||
return nil |
||||
} |
||||
|
||||
err := handlers.CreateLokiStack(context.TODO(), r, k) |
||||
require.NoError(t, err) |
||||
|
||||
// make sure create was called
|
||||
require.NotZero(t, k.CreateCallCount()) |
||||
} |
||||
@ -0,0 +1,84 @@ |
||||
package manifests |
||||
|
||||
import ( |
||||
"reflect" |
||||
|
||||
"github.com/ViaQ/logerr/kverrors" |
||||
appsv1 "k8s.io/api/apps/v1" |
||||
corev1 "k8s.io/api/core/v1" |
||||
"sigs.k8s.io/controller-runtime/pkg/client" |
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" |
||||
) |
||||
|
||||
// MutateFuncFor returns a mutate function based on the
|
||||
// existing resource's concrete type. It supports currently
|
||||
// only the following types or else panics:
|
||||
// - ConfigMap
|
||||
// - Service
|
||||
// - Deployment
|
||||
// - StatefulSet
|
||||
func MutateFuncFor(existing, desired client.Object) controllerutil.MutateFn { |
||||
return func() error { |
||||
existing.SetAnnotations(desired.GetAnnotations()) |
||||
existing.SetLabels(desired.GetLabels()) |
||||
|
||||
switch existing.(type) { |
||||
case *corev1.ConfigMap: |
||||
cm := existing.(*corev1.ConfigMap) |
||||
wantCm := desired.(*corev1.ConfigMap) |
||||
mutateConfigMap(cm, wantCm) |
||||
|
||||
case *corev1.Service: |
||||
svc := existing.(*corev1.Service) |
||||
wantSvc := desired.(*corev1.Service) |
||||
mutateService(svc, wantSvc) |
||||
|
||||
case *appsv1.Deployment: |
||||
dpl := existing.(*appsv1.Deployment) |
||||
wantDpl := desired.(*appsv1.Deployment) |
||||
mutateDeployment(dpl, wantDpl) |
||||
|
||||
case *appsv1.StatefulSet: |
||||
sts := existing.(*appsv1.StatefulSet) |
||||
wantSts := desired.(*appsv1.StatefulSet) |
||||
mutateStatefulSet(sts, wantSts) |
||||
|
||||
default: |
||||
t := reflect.TypeOf(existing).String() |
||||
return kverrors.New("missing mutate implementation for resource type", "type", t) |
||||
} |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
func mutateConfigMap(existing, desired *corev1.ConfigMap) { |
||||
existing.BinaryData = desired.BinaryData |
||||
} |
||||
|
||||
func mutateService(existing, desired *corev1.Service) { |
||||
existing.Spec.Ports = desired.Spec.Ports |
||||
existing.Spec.Selector = desired.Spec.Selector |
||||
} |
||||
|
||||
func mutateDeployment(existing, desired *appsv1.Deployment) { |
||||
// Deployment selector is immutable so we set this value only if
|
||||
// a new object is going to be created
|
||||
if existing.CreationTimestamp.IsZero() { |
||||
existing.Spec.Selector = desired.Spec.Selector |
||||
} |
||||
existing.Spec.Replicas = desired.Spec.Replicas |
||||
existing.Spec.Template = desired.Spec.Template |
||||
existing.Spec.Strategy = desired.Spec.Strategy |
||||
} |
||||
|
||||
func mutateStatefulSet(existing, desired *appsv1.StatefulSet) { |
||||
// StatefulSet selector is immutable so we set this value only if
|
||||
// a new object is going to be created
|
||||
if existing.CreationTimestamp.IsZero() { |
||||
existing.Spec.Selector = desired.Spec.Selector |
||||
} |
||||
existing.Spec.PodManagementPolicy = desired.Spec.PodManagementPolicy |
||||
existing.Spec.Replicas = desired.Spec.Replicas |
||||
existing.Spec.Template = desired.Spec.Template |
||||
existing.Spec.VolumeClaimTemplates = desired.Spec.VolumeClaimTemplates |
||||
} |
||||
@ -0,0 +1,410 @@ |
||||
package manifests |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
appsv1 "k8s.io/api/apps/v1" |
||||
corev1 "k8s.io/api/core/v1" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/util/intstr" |
||||
"k8s.io/utils/pointer" |
||||
) |
||||
|
||||
func TestGetMutateFunc_MutateObjectMeta(t *testing.T) { |
||||
got := &corev1.ConfigMap{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Labels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
Annotations: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
want := &corev1.ConfigMap{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Labels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
Annotations: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
f := MutateFuncFor(got, want) |
||||
err := f() |
||||
require.NoError(t, err) |
||||
|
||||
// Partial mutation checks
|
||||
require.Exactly(t, got.Labels, want.Labels) |
||||
require.Exactly(t, got.Annotations, want.Annotations) |
||||
} |
||||
|
||||
func TestGetMutateFunc_ReturnErrOnNotSupportedType(t *testing.T) { |
||||
got := &corev1.ServiceAccount{} |
||||
want := &corev1.ServiceAccount{} |
||||
f := MutateFuncFor(got, want) |
||||
|
||||
require.Error(t, f()) |
||||
} |
||||
|
||||
func TestGetMutateFunc_MutateConfigMap(t *testing.T) { |
||||
got := &corev1.ConfigMap{ |
||||
Data: map[string]string{"test": "remain"}, |
||||
BinaryData: map[string][]byte{}, |
||||
} |
||||
|
||||
want := &corev1.ConfigMap{ |
||||
Data: map[string]string{"test": "test"}, |
||||
BinaryData: map[string][]byte{"btest": []byte("btestss")}, |
||||
} |
||||
|
||||
f := MutateFuncFor(got, want) |
||||
err := f() |
||||
require.NoError(t, err) |
||||
|
||||
// Ensure partial mutation applied
|
||||
require.Equal(t, got.Labels, want.Labels) |
||||
require.Equal(t, got.Annotations, want.Annotations) |
||||
require.Equal(t, got.BinaryData, got.BinaryData) |
||||
|
||||
// Ensure not mutated
|
||||
require.NotEqual(t, got.Data, want.Data) |
||||
} |
||||
|
||||
func TestGetMutateFunc_MutateServiceSpec(t *testing.T) { |
||||
got := &corev1.Service{ |
||||
Spec: corev1.ServiceSpec{ |
||||
ClusterIP: "none", |
||||
ClusterIPs: []string{"8.8.8.8"}, |
||||
Ports: []corev1.ServicePort{ |
||||
{ |
||||
Protocol: corev1.ProtocolTCP, |
||||
Port: 7777, |
||||
TargetPort: intstr.FromString("8888"), |
||||
}, |
||||
}, |
||||
Selector: map[string]string{ |
||||
"select": "that", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
want := &corev1.Service{ |
||||
Spec: corev1.ServiceSpec{ |
||||
ClusterIP: "none", |
||||
ClusterIPs: []string{"8.8.8.8", "9.9.9.9"}, |
||||
Ports: []corev1.ServicePort{ |
||||
{ |
||||
Protocol: corev1.ProtocolTCP, |
||||
Port: 9999, |
||||
TargetPort: intstr.FromString("1111"), |
||||
}, |
||||
}, |
||||
Selector: map[string]string{ |
||||
"select": "that", |
||||
"and": "other", |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
f := MutateFuncFor(got, want) |
||||
err := f() |
||||
require.NoError(t, err) |
||||
|
||||
// Ensure partial mutation applied
|
||||
require.ElementsMatch(t, got.Spec.Ports, want.Spec.Ports) |
||||
require.Exactly(t, got.Spec.Selector, want.Spec.Selector) |
||||
|
||||
// Ensure not mutated
|
||||
require.Equal(t, got.Spec.ClusterIP, "none") |
||||
require.Exactly(t, got.Spec.ClusterIPs, []string{"8.8.8.8"}) |
||||
} |
||||
|
||||
func TestGeMutateFunc_MutateDeploymentSpec(t *testing.T) { |
||||
type test struct { |
||||
name string |
||||
got *appsv1.Deployment |
||||
want *appsv1.Deployment |
||||
} |
||||
table := []test{ |
||||
{ |
||||
name: "initial creation", |
||||
got: &appsv1.Deployment{ |
||||
Spec: appsv1.DeploymentSpec{ |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(1), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{Name: "test"}, |
||||
}, |
||||
}, |
||||
}, |
||||
Strategy: appsv1.DeploymentStrategy{ |
||||
Type: appsv1.RecreateDeploymentStrategyType, |
||||
}, |
||||
}, |
||||
}, |
||||
want: &appsv1.Deployment{ |
||||
Spec: appsv1.DeploymentSpec{ |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
"and": "another", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(2), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{ |
||||
Name: "test", |
||||
Args: []string{"--do-nothing"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
Strategy: appsv1.DeploymentStrategy{ |
||||
Type: appsv1.RollingUpdateDeploymentStrategyType, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "update spec without selector", |
||||
got: &appsv1.Deployment{ |
||||
ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, |
||||
Spec: appsv1.DeploymentSpec{ |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(1), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{Name: "test"}, |
||||
}, |
||||
}, |
||||
}, |
||||
Strategy: appsv1.DeploymentStrategy{ |
||||
Type: appsv1.RecreateDeploymentStrategyType, |
||||
}, |
||||
}, |
||||
}, |
||||
want: &appsv1.Deployment{ |
||||
ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, |
||||
Spec: appsv1.DeploymentSpec{ |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
"and": "another", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(2), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{ |
||||
Name: "test", |
||||
Args: []string{"--do-nothing"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
Strategy: appsv1.DeploymentStrategy{ |
||||
Type: appsv1.RollingUpdateDeploymentStrategyType, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tst := range table { |
||||
tst := tst |
||||
t.Run(tst.name, func(t *testing.T) { |
||||
t.Parallel() |
||||
f := MutateFuncFor(tst.got, tst.want) |
||||
err := f() |
||||
require.NoError(t, err) |
||||
|
||||
// Ensure conditional mutation applied
|
||||
if tst.got.CreationTimestamp.IsZero() { |
||||
require.Equal(t, tst.got.Spec.Selector, tst.want.Spec.Selector) |
||||
} else { |
||||
require.NotEqual(t, tst.got.Spec.Selector, tst.want.Spec.Selector) |
||||
} |
||||
|
||||
// Ensure partial mutation applied
|
||||
require.Equal(t, tst.got.Spec.Replicas, tst.want.Spec.Replicas) |
||||
require.Equal(t, tst.got.Spec.Template, tst.want.Spec.Template) |
||||
require.Equal(t, tst.got.Spec.Strategy, tst.want.Spec.Strategy) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGeMutateFunc_MutateStatefulSetSpec(t *testing.T) { |
||||
type test struct { |
||||
name string |
||||
got *appsv1.StatefulSet |
||||
want *appsv1.StatefulSet |
||||
} |
||||
table := []test{ |
||||
{ |
||||
name: "initial creation", |
||||
got: &appsv1.StatefulSet{ |
||||
Spec: appsv1.StatefulSetSpec{ |
||||
PodManagementPolicy: appsv1.ParallelPodManagement, |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(1), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{Name: "test"}, |
||||
}, |
||||
}, |
||||
}, |
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ |
||||
{ |
||||
Spec: corev1.PersistentVolumeClaimSpec{ |
||||
AccessModes: []corev1.PersistentVolumeAccessMode{ |
||||
corev1.ReadWriteOnce, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
want: &appsv1.StatefulSet{ |
||||
Spec: appsv1.StatefulSetSpec{ |
||||
PodManagementPolicy: appsv1.OrderedReadyPodManagement, |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
"and": "another", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(2), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{ |
||||
Name: "test", |
||||
Args: []string{"--do-nothing"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ |
||||
{ |
||||
Spec: corev1.PersistentVolumeClaimSpec{ |
||||
AccessModes: []corev1.PersistentVolumeAccessMode{ |
||||
corev1.ReadWriteOnce, |
||||
corev1.ReadOnlyMany, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "update spec without selector", |
||||
got: &appsv1.StatefulSet{ |
||||
ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, |
||||
Spec: appsv1.StatefulSetSpec{ |
||||
PodManagementPolicy: appsv1.ParallelPodManagement, |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(1), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{Name: "test"}, |
||||
}, |
||||
}, |
||||
}, |
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ |
||||
{ |
||||
Spec: corev1.PersistentVolumeClaimSpec{ |
||||
AccessModes: []corev1.PersistentVolumeAccessMode{ |
||||
corev1.ReadWriteOnce, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
want: &appsv1.StatefulSet{ |
||||
ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Now()}, |
||||
Spec: appsv1.StatefulSetSpec{ |
||||
PodManagementPolicy: appsv1.OrderedReadyPodManagement, |
||||
Selector: &metav1.LabelSelector{ |
||||
MatchLabels: map[string]string{ |
||||
"test": "test", |
||||
"and": "another", |
||||
}, |
||||
}, |
||||
Replicas: pointer.Int32Ptr(2), |
||||
Template: corev1.PodTemplateSpec{ |
||||
Spec: corev1.PodSpec{ |
||||
Containers: []corev1.Container{ |
||||
{ |
||||
Name: "test", |
||||
Args: []string{"--do-nothing"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ |
||||
{ |
||||
Spec: corev1.PersistentVolumeClaimSpec{ |
||||
AccessModes: []corev1.PersistentVolumeAccessMode{ |
||||
corev1.ReadWriteOnce, |
||||
corev1.ReadWriteMany, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tst := range table { |
||||
tst := tst |
||||
t.Run(tst.name, func(t *testing.T) { |
||||
t.Parallel() |
||||
f := MutateFuncFor(tst.got, tst.want) |
||||
err := f() |
||||
require.NoError(t, err) |
||||
|
||||
// Ensure conditional mutation applied
|
||||
if tst.got.CreationTimestamp.IsZero() { |
||||
require.Equal(t, tst.got.Spec.Selector, tst.want.Spec.Selector) |
||||
} else { |
||||
require.NotEqual(t, tst.got.Spec.Selector, tst.want.Spec.Selector) |
||||
} |
||||
|
||||
// Ensure partial mutation applied
|
||||
require.Equal(t, tst.got.Spec.Replicas, tst.want.Spec.Replicas) |
||||
require.Equal(t, tst.got.Spec.Template, tst.want.Spec.Template) |
||||
require.Equal(t, tst.got.Spec.VolumeClaimTemplates, tst.got.Spec.VolumeClaimTemplates) |
||||
}) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue