diff --git a/controllers/lokistack_controller.go b/controllers/lokistack_controller.go index 018ff4f2e8..8c24666bc5 100644 --- a/controllers/lokistack_controller.go +++ b/controllers/lokistack_controller.go @@ -54,7 +54,7 @@ type LokiStackReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile func (r *LokiStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - err := handlers.CreateLokiStack(ctx, req, r.Client) + err := handlers.CreateOrUpdateLokiStack(ctx, req, r.Client, r.Scheme) if err != nil { return ctrl.Result{ Requeue: true, @@ -67,8 +67,11 @@ func (r *LokiStackReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // SetupWithManager sets up the controller with the Manager. func (r *LokiStackReconciler) SetupWithManager(mgr ctrl.Manager) error { - createPredicate := predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { return false }, + filter := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Update only if generation changes, filter out anything else + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, CreateFunc: func(e event.CreateEvent) bool { return true }, DeleteFunc: func(e event.DeleteEvent) bool { return false }, GenericFunc: func(e event.GenericEvent) bool { return false }, @@ -76,6 +79,6 @@ func (r *LokiStackReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&lokiv1beta1.LokiStack{}). - WithEventFilter(createPredicate). + WithEventFilter(filter). Complete(r) } diff --git a/internal/external/k8s/client.go b/internal/external/k8s/client.go index fa35ea6343..30fbe0128e 100644 --- a/internal/external/k8s/client.go +++ b/internal/external/k8s/client.go @@ -3,6 +3,9 @@ package k8s import ( "context" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -13,4 +16,14 @@ import ( type Client interface { Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error Get(ctx context.Context, key client.ObjectKey, obj client.Object) error + Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error + Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error + DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error + List(ctx context.Context, obj client.ObjectList, opts ...client.ListOption) error + Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error + + Status() client.StatusWriter + + RESTMapper() meta.RESTMapper + Scheme() *runtime.Scheme } diff --git a/internal/external/k8s/k8sfakes/fake_client.go b/internal/external/k8s/k8sfakes/fake_client.go index 628bbcf79d..cf7646414f 100644 --- a/internal/external/k8s/k8sfakes/fake_client.go +++ b/internal/external/k8s/k8sfakes/fake_client.go @@ -6,6 +6,8 @@ import ( "sync" "github.com/ViaQ/loki-operator/internal/external/k8s" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -24,6 +26,32 @@ type FakeClient struct { createReturnsOnCall map[int]struct { result1 error } + DeleteStub func(context.Context, client.Object, ...client.DeleteOption) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 context.Context + arg2 client.Object + arg3 []client.DeleteOption + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + DeleteAllOfStub func(context.Context, client.Object, ...client.DeleteAllOfOption) error + deleteAllOfMutex sync.RWMutex + deleteAllOfArgsForCall []struct { + arg1 context.Context + arg2 client.Object + arg3 []client.DeleteAllOfOption + } + deleteAllOfReturns struct { + result1 error + } + deleteAllOfReturnsOnCall map[int]struct { + result1 error + } GetStub func(context.Context, types.NamespacedName, client.Object) error getMutex sync.RWMutex getArgsForCall []struct { @@ -37,6 +65,76 @@ type FakeClient struct { getReturnsOnCall map[int]struct { result1 error } + ListStub func(context.Context, client.ObjectList, ...client.ListOption) error + listMutex sync.RWMutex + listArgsForCall []struct { + arg1 context.Context + arg2 client.ObjectList + arg3 []client.ListOption + } + listReturns struct { + result1 error + } + listReturnsOnCall map[int]struct { + result1 error + } + PatchStub func(context.Context, client.Object, client.Patch, ...client.PatchOption) error + patchMutex sync.RWMutex + patchArgsForCall []struct { + arg1 context.Context + arg2 client.Object + arg3 client.Patch + arg4 []client.PatchOption + } + patchReturns struct { + result1 error + } + patchReturnsOnCall map[int]struct { + result1 error + } + RESTMapperStub func() meta.RESTMapper + rESTMapperMutex sync.RWMutex + rESTMapperArgsForCall []struct { + } + rESTMapperReturns struct { + result1 meta.RESTMapper + } + rESTMapperReturnsOnCall map[int]struct { + result1 meta.RESTMapper + } + SchemeStub func() *runtime.Scheme + schemeMutex sync.RWMutex + schemeArgsForCall []struct { + } + schemeReturns struct { + result1 *runtime.Scheme + } + schemeReturnsOnCall map[int]struct { + result1 *runtime.Scheme + } + StatusStub func() client.StatusWriter + statusMutex sync.RWMutex + statusArgsForCall []struct { + } + statusReturns struct { + result1 client.StatusWriter + } + statusReturnsOnCall map[int]struct { + result1 client.StatusWriter + } + UpdateStub func(context.Context, client.Object, ...client.UpdateOption) error + updateMutex sync.RWMutex + updateArgsForCall []struct { + arg1 context.Context + arg2 client.Object + arg3 []client.UpdateOption + } + updateReturns struct { + result1 error + } + updateReturnsOnCall map[int]struct { + result1 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -104,6 +202,132 @@ func (fake *FakeClient) CreateReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeClient) Delete(arg1 context.Context, arg2 client.Object, arg3 ...client.DeleteOption) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 context.Context + arg2 client.Object + arg3 []client.DeleteOption + }{arg1, arg2, arg3}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1, arg2, arg3}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3...) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeClient) DeleteCalls(stub func(context.Context, client.Object, ...client.DeleteOption) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeClient) DeleteArgsForCall(i int) (context.Context, client.Object, []client.DeleteOption) { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeClient) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) DeleteAllOf(arg1 context.Context, arg2 client.Object, arg3 ...client.DeleteAllOfOption) error { + fake.deleteAllOfMutex.Lock() + ret, specificReturn := fake.deleteAllOfReturnsOnCall[len(fake.deleteAllOfArgsForCall)] + fake.deleteAllOfArgsForCall = append(fake.deleteAllOfArgsForCall, struct { + arg1 context.Context + arg2 client.Object + arg3 []client.DeleteAllOfOption + }{arg1, arg2, arg3}) + stub := fake.DeleteAllOfStub + fakeReturns := fake.deleteAllOfReturns + fake.recordInvocation("DeleteAllOf", []interface{}{arg1, arg2, arg3}) + fake.deleteAllOfMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3...) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) DeleteAllOfCallCount() int { + fake.deleteAllOfMutex.RLock() + defer fake.deleteAllOfMutex.RUnlock() + return len(fake.deleteAllOfArgsForCall) +} + +func (fake *FakeClient) DeleteAllOfCalls(stub func(context.Context, client.Object, ...client.DeleteAllOfOption) error) { + fake.deleteAllOfMutex.Lock() + defer fake.deleteAllOfMutex.Unlock() + fake.DeleteAllOfStub = stub +} + +func (fake *FakeClient) DeleteAllOfArgsForCall(i int) (context.Context, client.Object, []client.DeleteAllOfOption) { + fake.deleteAllOfMutex.RLock() + defer fake.deleteAllOfMutex.RUnlock() + argsForCall := fake.deleteAllOfArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeClient) DeleteAllOfReturns(result1 error) { + fake.deleteAllOfMutex.Lock() + defer fake.deleteAllOfMutex.Unlock() + fake.DeleteAllOfStub = nil + fake.deleteAllOfReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) DeleteAllOfReturnsOnCall(i int, result1 error) { + fake.deleteAllOfMutex.Lock() + defer fake.deleteAllOfMutex.Unlock() + fake.DeleteAllOfStub = nil + if fake.deleteAllOfReturnsOnCall == nil { + fake.deleteAllOfReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteAllOfReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeClient) Get(arg1 context.Context, arg2 types.NamespacedName, arg3 client.Object) error { fake.getMutex.Lock() ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] @@ -167,13 +391,378 @@ func (fake *FakeClient) GetReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeClient) List(arg1 context.Context, arg2 client.ObjectList, arg3 ...client.ListOption) error { + fake.listMutex.Lock() + ret, specificReturn := fake.listReturnsOnCall[len(fake.listArgsForCall)] + fake.listArgsForCall = append(fake.listArgsForCall, struct { + arg1 context.Context + arg2 client.ObjectList + arg3 []client.ListOption + }{arg1, arg2, arg3}) + stub := fake.ListStub + fakeReturns := fake.listReturns + fake.recordInvocation("List", []interface{}{arg1, arg2, arg3}) + fake.listMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3...) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) ListCallCount() int { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + return len(fake.listArgsForCall) +} + +func (fake *FakeClient) ListCalls(stub func(context.Context, client.ObjectList, ...client.ListOption) error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.ListStub = stub +} + +func (fake *FakeClient) ListArgsForCall(i int) (context.Context, client.ObjectList, []client.ListOption) { + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + argsForCall := fake.listArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeClient) ListReturns(result1 error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.ListStub = nil + fake.listReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) ListReturnsOnCall(i int, result1 error) { + fake.listMutex.Lock() + defer fake.listMutex.Unlock() + fake.ListStub = nil + if fake.listReturnsOnCall == nil { + fake.listReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.listReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) Patch(arg1 context.Context, arg2 client.Object, arg3 client.Patch, arg4 ...client.PatchOption) error { + fake.patchMutex.Lock() + ret, specificReturn := fake.patchReturnsOnCall[len(fake.patchArgsForCall)] + fake.patchArgsForCall = append(fake.patchArgsForCall, struct { + arg1 context.Context + arg2 client.Object + arg3 client.Patch + arg4 []client.PatchOption + }{arg1, arg2, arg3, arg4}) + stub := fake.PatchStub + fakeReturns := fake.patchReturns + fake.recordInvocation("Patch", []interface{}{arg1, arg2, arg3, arg4}) + fake.patchMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4...) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) PatchCallCount() int { + fake.patchMutex.RLock() + defer fake.patchMutex.RUnlock() + return len(fake.patchArgsForCall) +} + +func (fake *FakeClient) PatchCalls(stub func(context.Context, client.Object, client.Patch, ...client.PatchOption) error) { + fake.patchMutex.Lock() + defer fake.patchMutex.Unlock() + fake.PatchStub = stub +} + +func (fake *FakeClient) PatchArgsForCall(i int) (context.Context, client.Object, client.Patch, []client.PatchOption) { + fake.patchMutex.RLock() + defer fake.patchMutex.RUnlock() + argsForCall := fake.patchArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeClient) PatchReturns(result1 error) { + fake.patchMutex.Lock() + defer fake.patchMutex.Unlock() + fake.PatchStub = nil + fake.patchReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) PatchReturnsOnCall(i int, result1 error) { + fake.patchMutex.Lock() + defer fake.patchMutex.Unlock() + fake.PatchStub = nil + if fake.patchReturnsOnCall == nil { + fake.patchReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.patchReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) RESTMapper() meta.RESTMapper { + fake.rESTMapperMutex.Lock() + ret, specificReturn := fake.rESTMapperReturnsOnCall[len(fake.rESTMapperArgsForCall)] + fake.rESTMapperArgsForCall = append(fake.rESTMapperArgsForCall, struct { + }{}) + stub := fake.RESTMapperStub + fakeReturns := fake.rESTMapperReturns + fake.recordInvocation("RESTMapper", []interface{}{}) + fake.rESTMapperMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) RESTMapperCallCount() int { + fake.rESTMapperMutex.RLock() + defer fake.rESTMapperMutex.RUnlock() + return len(fake.rESTMapperArgsForCall) +} + +func (fake *FakeClient) RESTMapperCalls(stub func() meta.RESTMapper) { + fake.rESTMapperMutex.Lock() + defer fake.rESTMapperMutex.Unlock() + fake.RESTMapperStub = stub +} + +func (fake *FakeClient) RESTMapperReturns(result1 meta.RESTMapper) { + fake.rESTMapperMutex.Lock() + defer fake.rESTMapperMutex.Unlock() + fake.RESTMapperStub = nil + fake.rESTMapperReturns = struct { + result1 meta.RESTMapper + }{result1} +} + +func (fake *FakeClient) RESTMapperReturnsOnCall(i int, result1 meta.RESTMapper) { + fake.rESTMapperMutex.Lock() + defer fake.rESTMapperMutex.Unlock() + fake.RESTMapperStub = nil + if fake.rESTMapperReturnsOnCall == nil { + fake.rESTMapperReturnsOnCall = make(map[int]struct { + result1 meta.RESTMapper + }) + } + fake.rESTMapperReturnsOnCall[i] = struct { + result1 meta.RESTMapper + }{result1} +} + +func (fake *FakeClient) Scheme() *runtime.Scheme { + fake.schemeMutex.Lock() + ret, specificReturn := fake.schemeReturnsOnCall[len(fake.schemeArgsForCall)] + fake.schemeArgsForCall = append(fake.schemeArgsForCall, struct { + }{}) + stub := fake.SchemeStub + fakeReturns := fake.schemeReturns + fake.recordInvocation("Scheme", []interface{}{}) + fake.schemeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) SchemeCallCount() int { + fake.schemeMutex.RLock() + defer fake.schemeMutex.RUnlock() + return len(fake.schemeArgsForCall) +} + +func (fake *FakeClient) SchemeCalls(stub func() *runtime.Scheme) { + fake.schemeMutex.Lock() + defer fake.schemeMutex.Unlock() + fake.SchemeStub = stub +} + +func (fake *FakeClient) SchemeReturns(result1 *runtime.Scheme) { + fake.schemeMutex.Lock() + defer fake.schemeMutex.Unlock() + fake.SchemeStub = nil + fake.schemeReturns = struct { + result1 *runtime.Scheme + }{result1} +} + +func (fake *FakeClient) SchemeReturnsOnCall(i int, result1 *runtime.Scheme) { + fake.schemeMutex.Lock() + defer fake.schemeMutex.Unlock() + fake.SchemeStub = nil + if fake.schemeReturnsOnCall == nil { + fake.schemeReturnsOnCall = make(map[int]struct { + result1 *runtime.Scheme + }) + } + fake.schemeReturnsOnCall[i] = struct { + result1 *runtime.Scheme + }{result1} +} + +func (fake *FakeClient) Status() client.StatusWriter { + fake.statusMutex.Lock() + ret, specificReturn := fake.statusReturnsOnCall[len(fake.statusArgsForCall)] + fake.statusArgsForCall = append(fake.statusArgsForCall, struct { + }{}) + stub := fake.StatusStub + fakeReturns := fake.statusReturns + fake.recordInvocation("Status", []interface{}{}) + fake.statusMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) StatusCallCount() int { + fake.statusMutex.RLock() + defer fake.statusMutex.RUnlock() + return len(fake.statusArgsForCall) +} + +func (fake *FakeClient) StatusCalls(stub func() client.StatusWriter) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = stub +} + +func (fake *FakeClient) StatusReturns(result1 client.StatusWriter) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = nil + fake.statusReturns = struct { + result1 client.StatusWriter + }{result1} +} + +func (fake *FakeClient) StatusReturnsOnCall(i int, result1 client.StatusWriter) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = nil + if fake.statusReturnsOnCall == nil { + fake.statusReturnsOnCall = make(map[int]struct { + result1 client.StatusWriter + }) + } + fake.statusReturnsOnCall[i] = struct { + result1 client.StatusWriter + }{result1} +} + +func (fake *FakeClient) Update(arg1 context.Context, arg2 client.Object, arg3 ...client.UpdateOption) error { + fake.updateMutex.Lock() + ret, specificReturn := fake.updateReturnsOnCall[len(fake.updateArgsForCall)] + fake.updateArgsForCall = append(fake.updateArgsForCall, struct { + arg1 context.Context + arg2 client.Object + arg3 []client.UpdateOption + }{arg1, arg2, arg3}) + stub := fake.UpdateStub + fakeReturns := fake.updateReturns + fake.recordInvocation("Update", []interface{}{arg1, arg2, arg3}) + fake.updateMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3...) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeClient) UpdateCallCount() int { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + return len(fake.updateArgsForCall) +} + +func (fake *FakeClient) UpdateCalls(stub func(context.Context, client.Object, ...client.UpdateOption) error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = stub +} + +func (fake *FakeClient) UpdateArgsForCall(i int) (context.Context, client.Object, []client.UpdateOption) { + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() + argsForCall := fake.updateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeClient) UpdateReturns(result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + fake.updateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeClient) UpdateReturnsOnCall(i int, result1 error) { + fake.updateMutex.Lock() + defer fake.updateMutex.Unlock() + fake.UpdateStub = nil + if fake.updateReturnsOnCall == nil { + fake.updateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeClient) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.createMutex.RLock() defer fake.createMutex.RUnlock() + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + fake.deleteAllOfMutex.RLock() + defer fake.deleteAllOfMutex.RUnlock() fake.getMutex.RLock() defer fake.getMutex.RUnlock() + fake.listMutex.RLock() + defer fake.listMutex.RUnlock() + fake.patchMutex.RLock() + defer fake.patchMutex.RUnlock() + fake.rESTMapperMutex.RLock() + defer fake.rESTMapperMutex.RUnlock() + fake.schemeMutex.RLock() + defer fake.schemeMutex.RUnlock() + fake.statusMutex.RLock() + defer fake.statusMutex.RUnlock() + fake.updateMutex.RLock() + defer fake.updateMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/handlers/lokistack_create.go b/internal/handlers/lokistack_create_or_update.go similarity index 63% rename from internal/handlers/lokistack_create.go rename to internal/handlers/lokistack_create_or_update.go index e7b14818fd..51e171375c 100644 --- a/internal/handlers/lokistack_create.go +++ b/internal/handlers/lokistack_create_or_update.go @@ -2,23 +2,26 @@ package handlers import ( "context" + "fmt" "os" "github.com/ViaQ/logerr/kverrors" "github.com/ViaQ/logerr/log" + lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1" "github.com/ViaQ/loki-operator/internal/external/k8s" "github.com/ViaQ/loki-operator/internal/manifests" + apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) -// CreateLokiStack handles a LokiStack create event -func CreateLokiStack(ctx context.Context, req ctrl.Request, k k8s.Client) error { - ll := log.WithValues("lokistack", req.NamespacedName, "event", "create") +// CreateOrUpdateLokiStack handles LokiStack create and update events. +func CreateOrUpdateLokiStack(ctx context.Context, req ctrl.Request, k k8s.Client, s *runtime.Scheme) error { + ll := log.WithValues("lokistack", req.NamespacedName, "event", "createOrUpdate") var stack lokiv1beta1.LokiStack if err := k.Get(ctx, req.NamespacedName, &stack); err != nil { @@ -51,30 +54,37 @@ func CreateLokiStack(ctx context.Context, req ctrl.Request, k k8s.Client) error return err } ll.Info("manifests built", "count", len(objects)) + + var errCount int32 for _, obj := range objects { - l := ll.WithValues("object_name", obj.GetName(), + l := ll.WithValues( + "object_name", obj.GetName(), "object_kind", obj.GetObjectKind(), - "object", obj) + ) obj.SetNamespace(req.Namespace) - setOwner(stack, obj) - if err := k.Create(ctx, obj); err != nil { - l.Error(err, "failed to create object") - // TODO requeue the event, but continue anyway + + if err := ctrl.SetControllerReference(&stack, obj, s); err != nil { + l.Error(err, "failed to set controller owner reference to resource") + errCount++ continue } - l.Info("Resource created", "resource", obj.GetName()) + + desired := obj.DeepCopyObject().(client.Object) + mutateFn := manifests.MutateFuncFor(obj, desired) + + op, err := ctrl.CreateOrUpdate(ctx, k, obj, mutateFn) + if err != nil { + l.Error(err, "failed to configure resource") + errCount++ + continue + } + l.Info(fmt.Sprintf("Resource has been %s", op)) } - return nil -} + if errCount > 0 { + return kverrors.New("failed to configure lokistack resources", "name", req.NamespacedName) + } -func setOwner(stack lokiv1beta1.LokiStack, o client.Object) { - o.SetOwnerReferences(append(o.GetOwnerReferences(), metav1.OwnerReference{ - APIVersion: lokiv1beta1.GroupVersion.String(), - Kind: stack.Kind, - Name: stack.Name, - UID: stack.UID, - Controller: pointer.BoolPtr(true), - })) + return nil } diff --git a/internal/handlers/lokistack_create_or_update_test.go b/internal/handlers/lokistack_create_or_update_test.go new file mode 100644 index 0000000000..814b473a74 --- /dev/null +++ b/internal/handlers/lokistack_create_or_update_test.go @@ -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) +} diff --git a/internal/handlers/lokistack_create_test.go b/internal/handlers/lokistack_create_test.go deleted file mode 100644 index a01036bf5b..0000000000 --- a/internal/handlers/lokistack_create_test.go +++ /dev/null @@ -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()) -} diff --git a/internal/manifests/mutate.go b/internal/manifests/mutate.go new file mode 100644 index 0000000000..0b3051e225 --- /dev/null +++ b/internal/manifests/mutate.go @@ -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 +} diff --git a/internal/manifests/mutate_test.go b/internal/manifests/mutate_test.go new file mode 100644 index 0000000000..5939bfdaba --- /dev/null +++ b/internal/manifests/mutate_test.go @@ -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) + }) + } +} diff --git a/internal/manifests/querier.go b/internal/manifests/querier.go index 4c1a99c829..1571e0ae74 100644 --- a/internal/manifests/querier.go +++ b/internal/manifests/querier.go @@ -197,7 +197,6 @@ func NewQuerierHTTPService(stackName string) *corev1.Service { Labels: l, }, Spec: corev1.ServiceSpec{ - ClusterIP: "None", Ports: []corev1.ServicePort{ { Name: "http", diff --git a/internal/manifests/query-frontend.go b/internal/manifests/query-frontend.go index 65fd1d519f..11b241440e 100644 --- a/internal/manifests/query-frontend.go +++ b/internal/manifests/query-frontend.go @@ -179,7 +179,6 @@ func NewQueryFrontendHTTPService(stackName string) *corev1.Service { Labels: l, }, Spec: corev1.ServiceSpec{ - ClusterIP: "None", Ports: []corev1.ServicePort{ { Name: "http",