diff --git a/integrations/operator/apis/resources/v1/loginrule_types.go b/integrations/operator/apis/resources/v1/loginrule_types.go index 0886e01632d2c..b808cdac9bf1d 100644 --- a/integrations/operator/apis/resources/v1/loginrule_types.go +++ b/integrations/operator/apis/resources/v1/loginrule_types.go @@ -107,8 +107,8 @@ func (l *LoginRuleResource) SetOrigin(origin string) { l.LoginRule.Metadata.SetOrigin(origin) } -func (l *LoginRuleResource) GetMetadata() types.Metadata { - return *l.LoginRule.Metadata +func (l *LoginRuleResource) Origin() string { + return l.LoginRule.Metadata.Origin() } func (l *LoginRuleResource) GetRevision() string { diff --git a/integrations/operator/controllers/reconciler.go b/integrations/operator/controllers/reconciler.go new file mode 100644 index 0000000000000..6d14b7535e952 --- /dev/null +++ b/integrations/operator/controllers/reconciler.go @@ -0,0 +1,32 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package controllers + +import ( + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// Reconciler extends the reconcile.Reconciler interface by adding a +// SetupWithManager function that creates a controller in the given manager. +// Every reconciler from the reconcilers package must implement this interface. +type Reconciler interface { + reconcile.Reconciler + SetupWithManager(mgr manager.Manager) error +} diff --git a/integrations/operator/controllers/resources/base_reconciler.go b/integrations/operator/controllers/reconcilers/base.go similarity index 82% rename from integrations/operator/controllers/resources/base_reconciler.go rename to integrations/operator/controllers/reconcilers/base.go index bbf199841d9c2..a19642cf556a6 100644 --- a/integrations/operator/controllers/resources/base_reconciler.go +++ b/integrations/operator/controllers/reconcilers/base.go @@ -1,6 +1,6 @@ /* * Teleport - * Copyright (C) 2023 Gravitational, Inc. + * Copyright (C) 2024 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package resources +package reconcilers import ( "context" @@ -30,15 +30,19 @@ import ( ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) +// TODO(hugoShaka) : merge the base reconciler with the generic reocnciler. +// This was a separate struct for backward compatibility but we removed the last +// controller relying directly on the base reconciler. + const ( - // DeletionFinalizer is a name of finalizer added to resource's 'finalizers' field + // DeletionFinalizer is a name of finalizer added to Resource's 'finalizers' field // for tracking deletion events. DeletionFinalizer = "resources.teleport.dev/deletion" // AnnotationFlagIgnore is the Kubernetes annotation containing the "ignore" flag. // When set to true, the operator will not reconcile the CR. AnnotationFlagIgnore = "teleport.dev/ignore" // AnnotationFlagKeep is the Kubernetes annotation containing the "keep" flag. - // When set to true, the operator will not delete the Teleport resource if the + // When set to true, the operator will not delete the Teleport Resource if the // CR is deleted. AnnotationFlagKeep = "teleport.dev/keep" ) @@ -53,26 +57,26 @@ type ResourceBaseReconciler struct { } /* -Do will receive an update request and reconcile the resource. +Do will receive an update request and reconcile the Resource. When an event arrives we must propagate that change into the Teleport cluster. We have two types of events: update/create and delete. -For creating/updating we check if the resource exists in Teleport +For creating/updating we check if the Resource exists in Teleport - if it does, we update it - otherwise we create it -Always using the state of the resource in the cluster as the source of truth. +Always using the state of the Resource in the cluster as the source of truth. For deleting, the recommendation is to use finalizers. -Finalizers allow us to map an external resource to a kubernetes resource. -So, when we create or update a resource, we add our own finalizer to the kubernetes resource list of finalizers. +Finalizers allow us to map an external Resource to a kubernetes Resource. +So, when we create or update a Resource, we add our own finalizer to the kubernetes Resource list of finalizers. -For a delete event which has our finalizer: the resource is deleted in Teleport. +For a delete event which has our finalizer: the Resource is deleted in Teleport. If it doesn't have the finalizer, we do nothing. ---- -Every time we update a resource in Kubernetes (adding finalizers or the OriginLabel), we end the reconciliation process. +Every time we update a Resource in Kubernetes (adding finalizers or the OriginLabel), we end the reconciliation process. Afterwards, we receive the request again and we progress to the next step. This allow us to progress with smaller changes and avoid a long-running reconciliation. */ @@ -85,7 +89,7 @@ func (r ResourceBaseReconciler) Do(ctx context.Context, req ctrl.Request, obj kc log.Info("not found") return ctrl.Result{}, nil } - log.Error(err, "failed to get resource") + log.Error(err, "failed to get Resource") return ctrl.Result{}, trace.Wrap(err) } @@ -140,7 +144,7 @@ func isIgnored(obj kclient.Object) bool { return checkAnnotationFlag(obj, AnnotationFlagIgnore, false /* defaults to false */) } -// isKept checks if the Teleport resource should be kept if the CR is deleted +// isKept checks if the Teleport Resource should be kept if the CR is deleted func isKept(obj kclient.Object) bool { return checkAnnotationFlag(obj, AnnotationFlagKeep, false /* defaults to false */) } diff --git a/integrations/operator/controllers/reconcilers/generic.go b/integrations/operator/controllers/reconcilers/generic.go new file mode 100644 index 0000000000000..13249adc31bd8 --- /dev/null +++ b/integrations/operator/controllers/reconcilers/generic.go @@ -0,0 +1,291 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package reconcilers + +import ( + "context" + "fmt" + "reflect" + + "github.com/gravitational/trace" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/gravitational/teleport/api/types" +) + +// Resource is any Teleport Resource the controller manages. +type Resource any + +// Adapter is an empty struct implementing helper functions for the reconciler +// to extract information from the Resource. This avoids having to implement the +// same interface on all resources. This became an issue as new resources are +// not implementing the types.Resource interface anymore. +type Adapter[T Resource] interface { + GetResourceName(T) string + GetResourceRevision(T) string + GetResourceOrigin(T) string + SetResourceRevision(T, string) + SetResourceLabels(T, map[string]string) +} + +// KubernetesCR is a Kubernetes CustomResource representing a Teleport Resource. +type KubernetesCR[T Resource] interface { + kclient.Object + ToTeleport() T + StatusConditions() *[]v1.Condition +} + +// resourceClient is a CRUD client for a specific Teleport Resource. +// Implementing this interface allows to be reconciled by the resourceReconciler +// instead of writing a new specific reconciliation loop. +// resourceClient implementations can optionally implement the resourceMutator +// and existingResourceMutator interfaces. +type resourceClient[T Resource] interface { + Get(context.Context, string) (T, error) + Create(context.Context, T) error + Update(context.Context, T) error + Delete(context.Context, string) error +} + +// resourceMutator can be implemented by resourceClients +// to edit a Resource before its creation/update. +type resourceMutator[T Resource] interface { + Mutate(new T) +} + +// existingResourceMutator can be implemented by TeleportResourceClients +// to edit a Resource before its update based on the existing one. +type existingResourceMutator[T Resource] interface { + MutateExisting(new, existing T) +} + +// resourceReconciler is a Teleport generic reconciler. +type resourceReconciler[T any, K KubernetesCR[T]] struct { + ResourceBaseReconciler + resourceClient resourceClient[T] + gvk schema.GroupVersionKind + adapter Adapter[T] +} + +// Upsert is the resourceReconciler of the ResourceBaseReconciler UpsertExternal +// It contains the logic to check if the Resource already exists, if it is owned by the operator and what +// to do to reconcile the Teleport Resource based on the Kubernetes one. +func (r resourceReconciler[T, K]) Upsert(ctx context.Context, obj kclient.Object) error { + debugLog := ctrllog.FromContext(ctx).V(1) + u, ok := obj.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("failed to convert Object into Resource object: %T", obj) + } + k8sResource := newKubeResource[K]() + debugLog.Info("Converting resource from unstructured", "crType", reflect.TypeOf(k8sResource)) + + // If an error happen we want to put it in status.conditions before returning. + err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation( + u.Object, + k8sResource, + true, /* returnUnknownFields */ + ) + updateErr := updateStatus(updateStatusConfig{ + ctx: ctx, + client: r.Client, + k8sResource: k8sResource, + condition: getStructureConditionFromError(err), + }) + if err != nil || updateErr != nil { + return trace.NewAggregate(err, updateErr) + } + + teleportResource := k8sResource.ToTeleport() + + debugLog.Info("Converting resource to teleport") + name := r.adapter.GetResourceName(teleportResource) + existingResource, err := r.resourceClient.Get(ctx, name) + updateErr = updateStatus(updateStatusConfig{ + ctx: ctx, + client: r.Client, + k8sResource: k8sResource, + condition: getReconciliationConditionFromError(err, true /* ignoreNotFound */), + }) + + if err != nil && !trace.IsNotFound(err) || updateErr != nil { + return trace.NewAggregate(err, updateErr) + } + // If err is nil, we found the Resource. If err != nil (and we did return), then the error was `NotFound` + exists := err == nil + + if exists { + debugLog.Info("Resource already exists") + newOwnershipCondition, isOwned := r.checkOwnership(existingResource) + debugLog.Info("Resource is owned") + if updateErr = updateStatus(updateStatusConfig{ + ctx: ctx, + client: r.Client, + k8sResource: k8sResource, + condition: newOwnershipCondition, + }); updateErr != nil { + return trace.Wrap(updateErr) + } + if !isOwned { + return trace.AlreadyExists("unowned Resource '%s' already exists", name) + } + } else { + debugLog.Info("Resource does not exist yet") + if updateErr = updateStatus(updateStatusConfig{ + ctx: ctx, + client: r.Client, + k8sResource: k8sResource, + condition: newResourceCondition, + }); updateErr != nil { + return trace.Wrap(updateErr) + } + } + + kubeLabels := obj.GetLabels() + teleportLabels := make(map[string]string, len(kubeLabels)+1) // +1 because we'll add the origin label + for k, v := range kubeLabels { + teleportLabels[k] = v + } + teleportLabels[types.OriginLabel] = types.OriginKubernetes + r.adapter.SetResourceLabels(teleportResource, teleportLabels) + debugLog.Info("Propagating labels from kube resource", "kubeLabels", kubeLabels, "teleportLabels", teleportLabels) + + if !exists { + // This is a new Resource + if mutator, ok := r.resourceClient.(resourceMutator[T]); ok { + debugLog.Info("Mutating new resource") + mutator.Mutate(teleportResource) + } + + err = r.resourceClient.Create(ctx, teleportResource) + } else { + // This is a Resource update, we must propagate the revision + currentRevision := r.adapter.GetResourceRevision(existingResource) + r.adapter.SetResourceRevision(teleportResource, currentRevision) + debugLog.Info("Propagating revision", "currentRevision", currentRevision) + if mutator, ok := r.resourceClient.(existingResourceMutator[T]); ok { + debugLog.Info("Mutating existing resource") + mutator.MutateExisting(teleportResource, existingResource) + } + + err = r.resourceClient.Update(ctx, teleportResource) + } + // If an error happens we want to put it in status.conditions before returning. + updateErr = updateStatus(updateStatusConfig{ + ctx: ctx, + client: r.Client, + k8sResource: k8sResource, + condition: getReconciliationConditionFromError(err, false /* ignoreNotFound */), + }) + + return trace.NewAggregate(err, updateErr) +} + +// Delete is the resourceReconciler of the ResourceBaseReconciler DeleteExertal +func (r resourceReconciler[T, K]) Delete(ctx context.Context, obj kclient.Object) error { + // This call catches non-existing resources or subkind mismatch (e.g. openssh nodes) + // We can then check that we own the Resource before deleting it. + resource, err := r.resourceClient.Get(ctx, obj.GetName()) + if err != nil { + return trace.Wrap(err) + } + + _, isOwned := r.checkOwnership(resource) + if !isOwned { + // The Resource doesn't belong to us, we bail out but unblock the CR deletion + return nil + } + // This GET->check->DELETE dance is race-prone, but it's good enough for what + // we want to do. No one should reconcile the same Resource as the operator. + // If they do, it's their fault as the Resource was clearly flagged as belonging to us. + return r.resourceClient.Delete(ctx, obj.GetName()) +} + +// Reconcile implements the controllers.Reconciler interface. +func (r resourceReconciler[T, K]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + obj, err := GetUnstructuredObjectFromGVK(r.gvk) + if err != nil { + return ctrl.Result{}, trace.Wrap(err, "creating object in which the CR will be unmarshalled") + } + return r.Do(ctx, req, obj) +} + +// SetupWithManager implements the controllers.Reconciler interface. +func (r resourceReconciler[T, K]) SetupWithManager(mgr ctrl.Manager) error { + // The resourceReconciler uses unstructured objects because of a silly json marshaling + // issue. Teleport's utils.String is a list of strings, but marshals as a single string if there's a single item. + // This is a questionable design as it breaks the openapi schema, but we're stuck with it. We had to relax openapi + // validation in those CRD fields, and use an unstructured object for the client, else JSON unmarshalling fails. + obj, err := GetUnstructuredObjectFromGVK(r.gvk) + if err != nil { + return trace.Wrap(err, "creating the model object for the manager watcher/client") + } + return ctrl. + NewControllerManagedBy(mgr). + For(obj). + WithEventFilter( + buildPredicate(), + ). + Complete(r) +} + +// isResourceOriginKubernetes reads a teleport Resource metadata, searches for the origin label and checks its +// value is kubernetes. +func (r resourceReconciler[T, K]) isResourceOriginKubernetes(resource T) bool { + origin := r.adapter.GetResourceOrigin(resource) + return origin == types.OriginKubernetes +} + +// checkOwnership takes an existing Resource and validates the operator owns it. +// It returns an ownership condition and a boolean representing if the Resource is +// owned by the operator. The ownedResource must be non-nil. +func (r resourceReconciler[T, K]) checkOwnership(existingResource T) (metav1.Condition, bool) { + if !r.isResourceOriginKubernetes(existingResource) { + // Existing Teleport Resource does not belong to us, bailing out + + condition := metav1.Condition{ + Type: ConditionTypeTeleportResourceOwned, + Status: metav1.ConditionFalse, + Reason: ConditionReasonOriginLabelNotMatching, + Message: "A Resource with the same name already exists in Teleport and does not have the Kubernetes origin label. Refusing to reconcile.", + } + return condition, false + } + + condition := metav1.Condition{ + Type: ConditionTypeTeleportResourceOwned, + Status: metav1.ConditionTrue, + Reason: ConditionReasonOriginLabelMatching, + Message: "Teleport Resource has the Kubernetes origin label.", + } + return condition, true +} + +var newResourceCondition = metav1.Condition{ + Type: ConditionTypeTeleportResourceOwned, + Status: metav1.ConditionTrue, + Reason: ConditionReasonNewResource, + Message: "No existing Teleport Resource found with that name. The created Resource is owned by the operator.", +} diff --git a/integrations/operator/controllers/reconcilers/generic_test.go b/integrations/operator/controllers/reconcilers/generic_test.go new file mode 100644 index 0000000000000..a16f97daabf81 --- /dev/null +++ b/integrations/operator/controllers/reconcilers/generic_test.go @@ -0,0 +1,307 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package reconcilers + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitational/teleport/api/defaults" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/operator/apis/resources" +) + +// newFakeTeleportResource creates a fakeTeleportResource +func newFakeTeleportResource(metadata types.Metadata) *fakeTeleportResource { + return &fakeTeleportResource{metadata: metadata} +} + +type fakeTeleportResource struct { + metadata types.Metadata +} + +func (r *fakeTeleportResource) GetMetadata() types.Metadata { + return r.metadata +} +func (r *fakeTeleportResource) SetMetadata(metadata types.Metadata) { + r.metadata = metadata +} + +// fakeTeleportResourceClient implements the TeleportResourceClient interface +// for the fakeTeleportResource. It mimics a teleport server by tracking the +// Resource state in its store. +type fakeTeleportResourceClient struct { + store map[string]types.Metadata +} + +// Get implements the TeleportResourceClient interface. +func (f *fakeTeleportResourceClient) Get(_ context.Context, name string) (*fakeTeleportResource, error) { + metadata, ok := f.store[name] + if !ok { + return nil, trace.NotFound("%q not found", name) + } + return newFakeTeleportResource(metadata), nil +} + +// Create implements the TeleportResourceClient interface. +func (f *fakeTeleportResourceClient) Create(_ context.Context, t *fakeTeleportResource) error { + name := t.GetMetadata().Name + _, ok := f.store[name] + if ok { + return trace.AlreadyExists("%q already exists", name) + } + metadata := t.GetMetadata() + metadata.SetRevision(uuid.New().String()) + f.store[name] = metadata + return nil +} + +// Update implements the TeleportResourceClient interface. +func (f *fakeTeleportResourceClient) Update(_ context.Context, t *fakeTeleportResource) error { + name := t.GetMetadata().Name + existing, ok := f.store[name] + if !ok { + return trace.NotFound("%q not found", name) + } + if existing.Revision != t.GetMetadata().Revision { + return trace.CompareFailed("revision mismatch") + } + metadata := t.GetMetadata() + metadata.SetRevision(uuid.New().String()) + f.store[name] = metadata + return nil +} + +// Delete implements the TeleportResourceClient interface. +func (f *fakeTeleportResourceClient) Delete(_ context.Context, name string) error { + _, ok := f.store[name] + if !ok { + return trace.NotFound("%q not found", name) + } + delete(f.store, name) + return nil + +} + +// resourceExists checks if a Resource is in the store. +// This is use fr testing purposes. +func (f *fakeTeleportResourceClient) resourceExists(name string) bool { + _, ok := f.store[name] + return ok +} + +// fakeTeleportKubernetesResource implements the TeleportKubernetesResource +// interface for testing purposes. +// Its corresponding TeleportResource is fakeTeleportResource. +type fakeTeleportKubernetesResource struct { + kclient.Object + status resources.Status +} + +// ToTeleport implements the TeleportKubernetesResource interface. +func (f *fakeTeleportKubernetesResource) ToTeleport() *fakeTeleportResource { + return &fakeTeleportResource{ + metadata: types.Metadata{ + Name: f.GetName(), + Namespace: defaults.Namespace, + Labels: map[string]string{}, + }, + } +} + +// StatusConditions implements the TeleportKubernetesResource interface. +func (f *fakeTeleportKubernetesResource) StatusConditions() *[]metav1.Condition { + return &f.status.Conditions +} + +type withMetadata interface { + GetMetadata() types.Metadata + SetMetadata(metadata types.Metadata) +} + +type fakeResourceAdapter[T withMetadata] struct{} + +func (f fakeResourceAdapter[T]) GetResourceName(res T) string { + return res.GetMetadata().Name +} + +func (f fakeResourceAdapter[T]) GetResourceRevision(res T) string { + return res.GetMetadata().Revision +} + +func (f fakeResourceAdapter[T]) GetResourceOrigin(res T) string { + labels := res.GetMetadata().Labels + if len(labels) == 0 { + return "" + } + if origin, ok := labels[types.OriginLabel]; ok { + return origin + } + return "" +} + +func (f fakeResourceAdapter[T]) SetResourceRevision(res T, rev string) { + metadata := res.GetMetadata() + metadata.SetRevision(rev) + res.SetMetadata(metadata) +} + +func (f fakeResourceAdapter[T]) SetResourceLabels(res T, labels map[string]string) { + metadata := res.GetMetadata() + metadata.Labels = labels + res.SetMetadata(metadata) +} + +func TestTeleportResourceReconciler_Delete(t *testing.T) { + ctx := context.Background() + resourceName := "test" + kubeResource := &unstructured.Unstructured{} + kubeResource.SetName(resourceName) + + tests := []struct { + name string + store map[string]types.Metadata + assertErr require.ErrorAssertionFunc + resourceExists bool + }{ + { + name: "delete non-existing Resource", + store: map[string]types.Metadata{}, + assertErr: func(t require.TestingT, err error, i ...interface{}) { + require.True(t, trace.IsNotFound(err)) + }, + resourceExists: false, + }, + { + name: "delete existing Resource", + store: map[string]types.Metadata{ + resourceName: { + Name: resourceName, + Labels: map[string]string{types.OriginLabel: types.OriginKubernetes}, + }, + }, + assertErr: require.NoError, + resourceExists: false, + }, + { + name: "delete existing but not owned Resource", + store: map[string]types.Metadata{ + resourceName: { + Name: resourceName, + Labels: map[string]string{}, + }, + }, + assertErr: require.NoError, + resourceExists: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resourceClient := &fakeTeleportResourceClient{tt.store} + reconciler := resourceReconciler[*fakeTeleportResource, *fakeTeleportKubernetesResource]{ + resourceClient: resourceClient, + adapter: fakeResourceAdapter[*fakeTeleportResource]{}, + } + tt.assertErr(t, reconciler.Delete(ctx, kubeResource)) + require.Equal(t, tt.resourceExists, resourceClient.resourceExists(resourceName)) + }) + } +} + +func TestCheckOwnership(t *testing.T) { + emptyStore := map[string]types.Metadata{} + rc := &fakeTeleportResourceClient{emptyStore} + reconciler := resourceReconciler[*fakeTeleportResource, *fakeTeleportKubernetesResource]{ + resourceClient: rc, + adapter: fakeResourceAdapter[*fakeTeleportResource]{}, + } + tests := []struct { + name string + existingResource *fakeTeleportResource + expectedConditionStatus metav1.ConditionStatus + expectedConditionReason string + isOwned bool + }{ + { + name: "existing owned Resource", + existingResource: &fakeTeleportResource{ + metadata: types.Metadata{ + Name: "existing owned user", + Labels: map[string]string{types.OriginLabel: types.OriginKubernetes}, + }, + }, + expectedConditionStatus: metav1.ConditionTrue, + expectedConditionReason: ConditionReasonOriginLabelMatching, + isOwned: true, + }, + { + name: "existing unowned Resource (no label)", + existingResource: &fakeTeleportResource{ + metadata: types.Metadata{ + Name: "existing unowned user without label", + }, + }, + expectedConditionStatus: metav1.ConditionFalse, + expectedConditionReason: ConditionReasonOriginLabelNotMatching, + isOwned: false, + }, + { + name: "existing unowned Resource (bad origin)", + existingResource: &fakeTeleportResource{ + metadata: types.Metadata{ + Name: "existing owned user without origin label", + Labels: map[string]string{types.OriginLabel: types.OriginConfigFile}, + }, + }, + expectedConditionStatus: metav1.ConditionFalse, + expectedConditionReason: ConditionReasonOriginLabelNotMatching, + isOwned: false, + }, + { + name: "existing unowned Resource (no origin)", + existingResource: &fakeTeleportResource{ + metadata: types.Metadata{ + Name: "existing owned user without origin label", + Labels: map[string]string{"foo": "bar"}, + }, + }, + expectedConditionStatus: metav1.ConditionFalse, + expectedConditionReason: ConditionReasonOriginLabelNotMatching, + isOwned: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + condition, isOwned := reconciler.checkOwnership(tc.existingResource) + + require.Equal(t, tc.isOwned, isOwned) + require.Equal(t, ConditionTypeTeleportResourceOwned, condition.Type) + require.Equal(t, tc.expectedConditionStatus, condition.Status) + require.Equal(t, tc.expectedConditionReason, condition.Reason) + }) + } +} diff --git a/integrations/operator/controllers/reconcilers/legacy_resource_with_labels.go b/integrations/operator/controllers/reconcilers/legacy_resource_with_labels.go new file mode 100644 index 0000000000000..86d751f170864 --- /dev/null +++ b/integrations/operator/controllers/reconcilers/legacy_resource_with_labels.go @@ -0,0 +1,79 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package reconcilers + +import ( + "github.com/gravitational/trace" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/operator/controllers" +) + +// ResourceWithLabelsAdapter implements the Adapter interface for any resource +// implementing types.ResourceWithLabels. +type ResourceWithLabelsAdapter[T types.ResourceWithLabels] struct { +} + +// GetResourceName implements the Adapter interface. +func (a ResourceWithLabelsAdapter[T]) GetResourceName(res T) string { + return res.GetName() +} + +// GetResourceRevision implements the Adapter interface. +func (a ResourceWithLabelsAdapter[T]) GetResourceRevision(res T) string { + return res.GetRevision() +} + +// GetResourceOrigin implements the Adapter interface. +func (a ResourceWithLabelsAdapter[T]) GetResourceOrigin(res T) string { + origin, _ := res.GetLabel(types.OriginLabel) + return origin +} + +// SetResourceRevision implements the Adapter interface. +func (a ResourceWithLabelsAdapter[T]) SetResourceRevision(res T, revision string) { + res.SetRevision(revision) +} + +// SetResourceLabels implements the Adapter interface. +func (a ResourceWithLabelsAdapter[T]) SetResourceLabels(res T, labels map[string]string) { + res.SetStaticLabels(labels) +} + +// NewTeleportResourceWithLabelsReconciler instantiates a resourceReconciler for a +// types.ResourceWithLabels resource. +func NewTeleportResourceWithLabelsReconciler[T types.ResourceWithLabels, K KubernetesCR[T]]( + client kclient.Client, + resourceClient resourceClient[T], +) (controllers.Reconciler, error) { + gvk, err := gvkFromScheme[K](controllers.Scheme) + if err != nil { + return nil, trace.Wrap(err) + } + reconciler := &resourceReconciler[T, K]{ + ResourceBaseReconciler: ResourceBaseReconciler{Client: client}, + resourceClient: resourceClient, + gvk: gvk, + adapter: ResourceWithLabelsAdapter[T]{}, + } + reconciler.ResourceBaseReconciler.UpsertExternal = reconciler.Upsert + reconciler.ResourceBaseReconciler.DeleteExternal = reconciler.Delete + return reconciler, nil +} diff --git a/integrations/operator/controllers/reconcilers/legacy_resource_without_labels.go b/integrations/operator/controllers/reconcilers/legacy_resource_without_labels.go new file mode 100644 index 0000000000000..c7079240a9d7b --- /dev/null +++ b/integrations/operator/controllers/reconcilers/legacy_resource_without_labels.go @@ -0,0 +1,93 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package reconcilers + +import ( + "github.com/gravitational/trace" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/operator/controllers" +) + +// resourceWithoutLabels is for resources that don't implement types.ResourceWithLabels +// but implement types.ResourceWithOrigin. This is a subset of types.ResourceWithOrigin. +type resourceWithoutLabels interface { + GetName() string + Origin() string + SetOrigin(string) + GetRevision() string + SetRevision(string) +} + +// ResourceWithoutLabelsAdapter implements the Adapter interface for resources +// implementing resourceWithoutLabels. +type ResourceWithoutLabelsAdapter[T resourceWithoutLabels] struct { +} + +// GetResourceName implements the Adapter interface. +func (a ResourceWithoutLabelsAdapter[T]) GetResourceName(res T) string { + return res.GetName() +} + +// GetResourceRevision implements the Adapter interface. +func (a ResourceWithoutLabelsAdapter[T]) GetResourceRevision(res T) string { + return res.GetRevision() +} + +// GetResourceOrigin implements the Adapter interface. +func (a ResourceWithoutLabelsAdapter[T]) GetResourceOrigin(res T) string { + return res.Origin() +} + +// SetResourceRevision implements the Adapter interface. +func (a ResourceWithoutLabelsAdapter[T]) SetResourceRevision(res T, revision string) { + res.SetRevision(revision) +} + +// SetResourceLabels implements the Adapter interface. As the resource does not +// // support labels, it only sets the origin label. +func (a ResourceWithoutLabelsAdapter[T]) SetResourceLabels(res T, labels map[string]string) { + // We don't set all labels as the Resource doesn't support them + // Only the origin + origin := labels[types.OriginLabel] + res.SetOrigin(origin) +} + +// NewTeleportResourceWithoutLabelsReconciler instantiates a resourceReconciler for a +// resource not implementing types.ResourcesWithLabels but implementing +// resourceWithoutLabels. +func NewTeleportResourceWithoutLabelsReconciler[T resourceWithoutLabels, K KubernetesCR[T]]( + client kclient.Client, + resourceClient resourceClient[T], +) (controllers.Reconciler, error) { + gvk, err := gvkFromScheme[K](controllers.Scheme) + if err != nil { + return nil, trace.Wrap(err) + } + reconciler := &resourceReconciler[T, K]{ + ResourceBaseReconciler: ResourceBaseReconciler{Client: client}, + resourceClient: resourceClient, + gvk: gvk, + adapter: ResourceWithoutLabelsAdapter[T]{}, + } + reconciler.ResourceBaseReconciler.UpsertExternal = reconciler.Upsert + reconciler.ResourceBaseReconciler.DeleteExternal = reconciler.Delete + return reconciler, nil +} diff --git a/integrations/operator/controllers/reconcilers/resource153.go b/integrations/operator/controllers/reconcilers/resource153.go new file mode 100644 index 0000000000000..95c83d69e37b0 --- /dev/null +++ b/integrations/operator/controllers/reconcilers/resource153.go @@ -0,0 +1,87 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package reconcilers + +import ( + "github.com/gravitational/trace" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integrations/operator/controllers" +) + +// Resource153Adapter implements the Adapter interface for any resource +// implementing types.Resource153. +type Resource153Adapter[T types.Resource153] struct{} + +// GetResourceName implements the Adapter interface. +func (a Resource153Adapter[T]) GetResourceName(res T) string { + return res.GetMetadata().GetName() +} + +// GetResourceRevision implements the Adapter interface. +func (a Resource153Adapter[T]) GetResourceRevision(res T) string { + return res.GetMetadata().GetRevision() +} + +// GetResourceOrigin implements the Adapter interface. +func (a Resource153Adapter[T]) GetResourceOrigin(res T) string { + labels := res.GetMetadata().GetLabels() + // catches nil and empty maps + if len(labels) == 0 { + return "" + } + + if origin, ok := labels[types.OriginLabel]; ok { + return origin + } + // Origin label is not set + return "" +} + +// SetResourceRevision implements the Adapter interface. +func (a Resource153Adapter[T]) SetResourceRevision(res T, revision string) { + res.GetMetadata().Revision = revision +} + +// SetResourceLabels implements the Adapter interface. +func (a Resource153Adapter[T]) SetResourceLabels(res T, labels map[string]string) { + res.GetMetadata().Labels = labels +} + +// NewTeleportResource153Reconciler instantiates a resourceReconciler for a +// types.Resource153 resource. +func NewTeleportResource153Reconciler[T types.Resource153, K KubernetesCR[T]]( + client kclient.Client, + resourceClient resourceClient[T], +) (controllers.Reconciler, error) { + gvk, err := gvkFromScheme[K](controllers.Scheme) + if err != nil { + return nil, trace.Wrap(err) + } + reconciler := &resourceReconciler[T, K]{ + ResourceBaseReconciler: ResourceBaseReconciler{Client: client}, + resourceClient: resourceClient, + gvk: gvk, + adapter: Resource153Adapter[T]{}, + } + reconciler.ResourceBaseReconciler.UpsertExternal = reconciler.Upsert + reconciler.ResourceBaseReconciler.DeleteExternal = reconciler.Delete + return reconciler, nil +} diff --git a/integrations/operator/controllers/resources/utils.go b/integrations/operator/controllers/reconcilers/utils.go similarity index 61% rename from integrations/operator/controllers/resources/utils.go rename to integrations/operator/controllers/reconcilers/utils.go index 32d48ece073f9..aaf53f1f6e9d4 100644 --- a/integrations/operator/controllers/resources/utils.go +++ b/integrations/operator/controllers/reconcilers/utils.go @@ -1,6 +1,6 @@ /* * Teleport - * Copyright (C) 2023 Gravitational, Inc. + * Copyright (C) 2024 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,22 +16,25 @@ * along with this program. If not, see . */ -package resources +package reconcilers import ( "context" "fmt" + "reflect" + "slices" "strconv" "github.com/gravitational/trace" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" kclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/gravitational/teleport/api/types" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) const ( @@ -41,66 +44,82 @@ const ( ConditionReasonNewResource = "NewResource" ConditionReasonNoError = "NoError" ConditionReasonTeleportError = "TeleportError" - ConditionReasonTeleportClientError = "TeleportClientError" ConditionTypeTeleportResourceOwned = "TeleportResourceOwned" ConditionTypeSuccessfullyReconciled = "SuccessfullyReconciled" ConditionTypeValidStructure = "ValidStructure" - ConditionTypeTeleportClient = "TeleportClient" ) -var newResourceCondition = metav1.Condition{ - Type: ConditionTypeTeleportResourceOwned, - Status: metav1.ConditionTrue, - Reason: ConditionReasonNewResource, - Message: "No existing Teleport resource found with that name. The created resource is owned by the operator.", -} - -type ownedResource interface { - GetMetadata() types.Metadata -} - -// isResourceOriginKubernetes reads a teleport resource metadata, searches for the origin label and checks its -// value is kubernetes. -func isResourceOriginKubernetes(resource ownedResource) bool { - label := resource.GetMetadata().Labels[types.OriginLabel] - return label == types.OriginKubernetes +// gvkFromScheme looks up the GVK from the runtime scheme. +// The structured type must have been registered before in the scheme. This function is used when you have a structured +// type, a scheme containing this structured type, and want to build an unstructured object for the same GVK. +func gvkFromScheme[K runtime.Object](scheme *runtime.Scheme) (schema.GroupVersionKind, error) { + structuredObj := newKubeResource[K]() + gvks, _, err := scheme.ObjectKinds(structuredObj) + if err != nil { + return schema.GroupVersionKind{}, trace.Wrap(err, "looking up gvk in scheme for type %T", structuredObj) + } + if len(gvks) != 1 { + return schema.GroupVersionKind{}, trace.CompareFailed( + "failed GVK lookup in scheme, looked up %T and got %d matches, expected 1", structuredObj, len(gvks), + ) + } + return gvks[0], nil } -// checkOwnership takes an existing resource and validates the operator owns it. -// It returns an ownership condition and a boolean representing if the resource is -// owned by the operator. The ownedResource must be non-nil. -func checkOwnership(existingResource ownedResource) (metav1.Condition, bool) { - if !isResourceOriginKubernetes(existingResource) { - // Existing Teleport resource does not belong to us, bailing out - - condition := metav1.Condition{ - Type: ConditionTypeTeleportResourceOwned, - Status: metav1.ConditionFalse, - Reason: ConditionReasonOriginLabelNotMatching, - Message: "A resource with the same name already exists in Teleport and does not have the Kubernetes origin label. Refusing to reconcile.", - } - return condition, false +// newKubeResource creates a new TeleportKubernetesResource +// the function supports structs or pointer to struct implementations of the TeleportKubernetesResource interface +func newKubeResource[K any]() K { + // We create a new instance of K. + var resource K + // We take the type of K + interfaceType := reflect.TypeOf(resource) + // If K is not a pointer we don't need to do anything + // If K is a pointer, new(K) is only initializing a nil pointer, we need to manually initialize its destination + if interfaceType.Kind() == reflect.Ptr { + // We create a new Value of the type pointed by K. reflect.New returns a pointer to this value + initializedResource := reflect.New(interfaceType.Elem()) + // We cast back to K + resource = initializedResource.Interface().(K) } + return resource +} - condition := metav1.Condition{ - Type: ConditionTypeTeleportResourceOwned, - Status: metav1.ConditionTrue, - Reason: ConditionReasonOriginLabelMatching, - Message: "Teleport resource has the Kubernetes origin label.", - } - return condition, true +// buildPredicate returns a predicate that triggers the reconciliation when: +// - the Resource generation changes +// - the Resource finalizers change +// - the Resource annotations change +// - the Resource labels change +// - the Resource is created +// - the Resource is deleted +// It does not trigger the reconciliation when: +// - the Resource status changes +func buildPredicate() predicate.Predicate { + return predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + predicate.LabelChangedPredicate{}, + predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + return !slices.Equal(e.ObjectNew.GetFinalizers(), e.ObjectOld.GetFinalizers()) + }, + }, + ) } // getReconciliationConditionFromError takes an error returned by a call to Teleport and returns a -// metav1.Condition describing how the Teleport resource reconciliation went. This is used to provide feedback to -// the user about the controller's ability to reconcile the resource. +// metav1.Condition describing how the Teleport Resource reconciliation went. This is used to provide feedback to +// the user about the controller's ability to reconcile the Resource. func getReconciliationConditionFromError(err error, ignoreNotFound bool) metav1.Condition { if err == nil || trace.IsNotFound(err) && ignoreNotFound { return metav1.Condition{ Type: ConditionTypeSuccessfullyReconciled, Status: metav1.ConditionTrue, Reason: ConditionReasonNoError, - Message: "Teleport resource was successfully reconciled, no error was returned by Teleport.", + Message: "Teleport Resource was successfully reconciled, no error was returned by Teleport.", } } return metav1.Condition{ @@ -113,7 +132,7 @@ func getReconciliationConditionFromError(err error, ignoreNotFound bool) metav1. // getStructureConditionFromError takes a conversion error from k8s apimachinery's runtime.UnstructuredConverter // and returns a metav1.Condition describing how the status conversion went. This is used to provide feedback to -// the user about the controller's ability to reconcile the resource. +// the user about the controller's ability to reconcile the Resource. func getStructureConditionFromError(err error) metav1.Condition { if err != nil { return metav1.Condition{ @@ -142,7 +161,7 @@ type updateStatusConfig struct { condition metav1.Condition } -// updateStatus updates the resource status but swallows the error if the update fails. +// updateStatus updates the Resource status but swallows the error if the update fails. func updateStatus(config updateStatusConfig) error { // If the condition is empty, we don't want to update the status. if config.condition == (metav1.Condition{}) { @@ -171,7 +190,7 @@ func GetUnstructuredObjectFromGVK(gvk schema.GroupVersionKind) (*unstructured.Un return &obj, nil } -// checkAnnotationFlag checks is the Kubernetes resource is annotated with a +// checkAnnotationFlag checks is the Kubernetes Resource is annotated with a // flag and parses its value. Returns the default value if the flag is missing // or the annotation value cannot be parsed. func checkAnnotationFlag(object kclient.Object, flagName string, defaultValue bool) bool { diff --git a/integrations/operator/controllers/resources/utils_test.go b/integrations/operator/controllers/reconcilers/utils_test.go similarity index 52% rename from integrations/operator/controllers/resources/utils_test.go rename to integrations/operator/controllers/reconcilers/utils_test.go index d22ed80697712..b725a5b163adc 100644 --- a/integrations/operator/controllers/resources/utils_test.go +++ b/integrations/operator/controllers/reconcilers/utils_test.go @@ -1,6 +1,6 @@ /* * Teleport - * Copyright (C) 2023 Gravitational, Inc. + * Copyright (C) 2024 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,87 +16,16 @@ * along with this program. If not, see . */ -package resources +package reconcilers import ( "testing" "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/gravitational/teleport/api/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) -func TestCheckOwnership(t *testing.T) { - tests := []struct { - name string - existingResource types.Resource - expectedConditionStatus metav1.ConditionStatus - expectedConditionReason string - isOwned bool - }{ - { - name: "existing owned resource", - existingResource: &types.UserV2{ - Metadata: types.Metadata{ - Name: "existing owned user", - Labels: map[string]string{types.OriginLabel: types.OriginKubernetes}, - }, - }, - expectedConditionStatus: metav1.ConditionTrue, - expectedConditionReason: ConditionReasonOriginLabelMatching, - isOwned: true, - }, - { - name: "existing unowned resource (no label)", - existingResource: &types.UserV2{ - Metadata: types.Metadata{ - Name: "existing unowned user without label", - }, - }, - expectedConditionStatus: metav1.ConditionFalse, - expectedConditionReason: ConditionReasonOriginLabelNotMatching, - isOwned: false, - }, - { - name: "existing unowned resource (bad origin)", - existingResource: &types.UserV2{ - Metadata: types.Metadata{ - Name: "existing owned user without origin label", - Labels: map[string]string{types.OriginLabel: types.OriginConfigFile}, - }, - }, - expectedConditionStatus: metav1.ConditionFalse, - expectedConditionReason: ConditionReasonOriginLabelNotMatching, - isOwned: false, - }, - { - name: "existing unowned resource (no origin)", - existingResource: &types.UserV2{ - Metadata: types.Metadata{ - Name: "existing owned user without origin label", - Labels: map[string]string{"foo": "bar"}, - }, - }, - expectedConditionStatus: metav1.ConditionFalse, - expectedConditionReason: ConditionReasonOriginLabelNotMatching, - isOwned: false, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - - condition, isOwned := checkOwnership(tc.existingResource) - - require.Equal(t, tc.isOwned, isOwned) - require.Equal(t, ConditionTypeTeleportResourceOwned, condition.Type) - require.Equal(t, tc.expectedConditionStatus, condition.Status) - require.Equal(t, tc.expectedConditionReason, condition.Reason) - }) - } -} - func TestCheckAnnotationFlag(t *testing.T) { testFlag := "foo" tests := []struct { @@ -164,3 +93,20 @@ func TestCheckAnnotationFlag(t *testing.T) { }) } } + +type fakeKubernetesResource struct { + client.Object +} + +func TestNewKubeResource(t *testing.T) { + // Test with a value receiver + resource := newKubeResource[fakeKubernetesResource]() + require.IsTypef(t, fakeKubernetesResource{}, resource, "Should be of type FakeKubernetesResource") + require.NotNil(t, resource) + + // Test with a pointer receiver + resourcePtr := newKubeResource[*fakeKubernetesResource]() + require.IsTypef(t, &fakeKubernetesResource{}, resourcePtr, "Should be a pointer on FakeKubernetesResourcePtrReceiver") + require.NotNil(t, resourcePtr) + require.NotNil(t, *resourcePtr) +} diff --git a/integrations/operator/controllers/resources/accesslist_controller.go b/integrations/operator/controllers/resources/accesslist_controller.go index 4321d904aa197..f44a4037cde95 100644 --- a/integrations/operator/controllers/resources/accesslist_controller.go +++ b/integrations/operator/controllers/resources/accesslist_controller.go @@ -25,6 +25,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types/accesslist" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // accessListClient implements TeleportResourceClient and offers CRUD methods needed to reconcile access_lists. @@ -63,12 +65,12 @@ func (r accessListClient) MutateExisting(new, existing *accesslist.AccessList) { } // NewAccessListReconciler instantiates a new Kubernetes controller reconciling access_list resources -func NewAccessListReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewAccessListReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { accessListClient := &accessListClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[*accesslist.AccessList, *resourcesv1.TeleportAccessList]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[*accesslist.AccessList, *resourcesv1.TeleportAccessList]( client, accessListClient, ) diff --git a/integrations/operator/controllers/resources/github_connector_controller.go b/integrations/operator/controllers/resources/github_connector_controller.go index 699beb8fb89e1..cb400d3ce4c77 100644 --- a/integrations/operator/controllers/resources/github_connector_controller.go +++ b/integrations/operator/controllers/resources/github_connector_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // githubConnectorClient implements TeleportResourceClient and offers CRUD methods needed to reconcile github_connectors @@ -58,12 +60,12 @@ func (r githubConnectorClient) Delete(ctx context.Context, name string) error { } // NewGithubConnectorReconciler instantiates a new Kubernetes controller reconciling github_connector resources -func NewGithubConnectorReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewGithubConnectorReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { githubClient := &githubConnectorClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.GithubConnector, *resourcesv3.TeleportGithubConnector]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[types.GithubConnector, *resourcesv3.TeleportGithubConnector]( client, githubClient, ) diff --git a/integrations/operator/controllers/resources/github_connector_controller_test.go b/integrations/operator/controllers/resources/github_connector_controller_test.go index 4ceb32c4eb504..321f61cfae731 100644 --- a/integrations/operator/controllers/resources/github_connector_controller_test.go +++ b/integrations/operator/controllers/resources/github_connector_controller_test.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -47,6 +48,7 @@ var githubSpec = types.GithubConnectorSpecV3{ type githubTestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithoutLabelsAdapter[types.GithubConnector] } func (g *githubTestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/login_rule_controller.go b/integrations/operator/controllers/resources/login_rule_controller.go index 4b78d4d971828..6726c3bf6b437 100644 --- a/integrations/operator/controllers/resources/login_rule_controller.go +++ b/integrations/operator/controllers/resources/login_rule_controller.go @@ -26,6 +26,8 @@ import ( "github.com/gravitational/teleport/api/client" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // loginRuleClient implements TeleportResourceClient and offers CRUD methods needed to reconcile login_rules @@ -61,12 +63,12 @@ func (l loginRuleClient) Delete(ctx context.Context, name string) error { } // NewLoginRuleReconciler instantiates a new Kubernetes controller reconciling login_rule resources -func NewLoginRuleReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewLoginRuleReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { loginRuleClient := &loginRuleClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[*resourcesv1.LoginRuleResource, *resourcesv1.TeleportLoginRule]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[*resourcesv1.LoginRuleResource, *resourcesv1.TeleportLoginRule]( client, loginRuleClient, ) diff --git a/integrations/operator/controllers/resources/newkuberesource_test.go b/integrations/operator/controllers/resources/newkuberesource_test.go deleted file mode 100644 index 04f3dcc672eef..0000000000000 --- a/integrations/operator/controllers/resources/newkuberesource_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package resources - -import ( - "testing" - - "github.com/stretchr/testify/require" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/gravitational/teleport/api/types" -) - -type FakeResourceWithOrigin types.GithubConnector - -type FakeKubernetesResource struct { - client.Object -} - -func (r FakeKubernetesResource) ToTeleport() FakeResourceWithOrigin { - return nil -} - -func (r FakeKubernetesResource) StatusConditions() *[]v1.Condition { - return nil -} - -type FakeKubernetesResourcePtrReceiver struct { - client.Object -} - -func (r *FakeKubernetesResourcePtrReceiver) ToTeleport() FakeResourceWithOrigin { - return nil -} - -func (r *FakeKubernetesResourcePtrReceiver) StatusConditions() *[]v1.Condition { - return nil -} - -func TestNewKubeResource(t *testing.T) { - // Test with a value receiver - resource := newKubeResource[FakeResourceWithOrigin, FakeKubernetesResource]() - require.IsTypef(t, FakeKubernetesResource{}, resource, "Should be of type FakeKubernetesResource") - require.NotNil(t, resource) - - // Test with a pointer receiver - resourcePtr := newKubeResource[FakeResourceWithOrigin, *FakeKubernetesResourcePtrReceiver]() - require.IsTypef(t, &FakeKubernetesResourcePtrReceiver{}, resourcePtr, "Should be a pointer on FakeKubernetesResourcePtrReceiver") - require.NotNil(t, resourcePtr) - require.NotNil(t, *resourcePtr) -} diff --git a/integrations/operator/controllers/resources/oidc_connector_controller.go b/integrations/operator/controllers/resources/oidc_connector_controller.go index fcdcaf797e248..d6f5cc3401846 100644 --- a/integrations/operator/controllers/resources/oidc_connector_controller.go +++ b/integrations/operator/controllers/resources/oidc_connector_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // oidcConnectorClient implements TeleportResourceClient and offers CRUD methods needed to reconcile oidc_connectors @@ -58,12 +60,12 @@ func (r oidcConnectorClient) Delete(ctx context.Context, name string) error { } // NewOIDCConnectorReconciler instantiates a new Kubernetes controller reconciling oidc_connector resources -func NewOIDCConnectorReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewOIDCConnectorReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { oidcClient := &oidcConnectorClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.OIDCConnector, *resourcesv3.TeleportOIDCConnector]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[types.OIDCConnector, *resourcesv3.TeleportOIDCConnector]( client, oidcClient, ) diff --git a/integrations/operator/controllers/resources/oidc_connector_controller_test.go b/integrations/operator/controllers/resources/oidc_connector_controller_test.go index 35b44d25b1fe3..5b3984ce7b6e0 100644 --- a/integrations/operator/controllers/resources/oidc_connector_controller_test.go +++ b/integrations/operator/controllers/resources/oidc_connector_controller_test.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -46,6 +47,7 @@ var oidcSpec = types.OIDCConnectorSpecV3{ type oidcTestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithoutLabelsAdapter[types.OIDCConnector] } func (g *oidcTestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/okta_import_rule_controller.go b/integrations/operator/controllers/resources/okta_import_rule_controller.go index 10beeca093374..c0afb4c3b6e3c 100644 --- a/integrations/operator/controllers/resources/okta_import_rule_controller.go +++ b/integrations/operator/controllers/resources/okta_import_rule_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // oktaImportRuleClient implements TeleportResourceClient and offers CRUD methods needed to reconcile okta_import_rules @@ -58,12 +60,12 @@ func (r oktaImportRuleClient) Delete(ctx context.Context, name string) error { } // NewOktaImportRuleReconciler instantiates a new Kubernetes controller reconciling okta_import_rule resources -func NewOktaImportRuleReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewOktaImportRuleReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { oktaImportRuleClient := &oktaImportRuleClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.OktaImportRule, *resourcesv1.TeleportOktaImportRule]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.OktaImportRule, *resourcesv1.TeleportOktaImportRule]( client, oktaImportRuleClient, ) diff --git a/integrations/operator/controllers/resources/okta_import_rule_controller_test.go b/integrations/operator/controllers/resources/okta_import_rule_controller_test.go index fed8662792cb0..aa719d5b34dd5 100644 --- a/integrations/operator/controllers/resources/okta_import_rule_controller_test.go +++ b/integrations/operator/controllers/resources/okta_import_rule_controller_test.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" "github.com/gravitational/teleport/lib/utils" ) @@ -61,6 +62,7 @@ var oktaImportRuleSpec = types.OktaImportRuleSpecV1{ type oktaImportRuleTestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithLabelsAdapter[types.OktaImportRule] } func (g *oktaImportRuleTestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/openssheiceserverv2_controller.go b/integrations/operator/controllers/resources/openssheiceserverv2_controller.go index 49ce2a24976e1..18866b4abbe2d 100644 --- a/integrations/operator/controllers/resources/openssheiceserverv2_controller.go +++ b/integrations/operator/controllers/resources/openssheiceserverv2_controller.go @@ -28,6 +28,8 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // openSSHEICEServerClient implements TeleportResourceClient and offers CRUD @@ -71,12 +73,12 @@ func (r openSSHEICEServerClient) Delete(ctx context.Context, name string) error // NewOpenSSHEICEServerV2Reconciler instantiates a new Kubernetes controller // reconciling OpenSSHEICE server resources. -func NewOpenSSHEICEServerV2Reconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewOpenSSHEICEServerV2Reconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { serverClient := &openSSHEICEServerClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.Server, *resourcesv1.TeleportOpenSSHEICEServerV2]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.Server, *resourcesv1.TeleportOpenSSHEICEServerV2]( client, serverClient, ) diff --git a/integrations/operator/controllers/resources/openssheiceserverv2_controller_test.go b/integrations/operator/controllers/resources/openssheiceserverv2_controller_test.go index 1bc01705b3f5d..fbfdebdc61774 100644 --- a/integrations/operator/controllers/resources/openssheiceserverv2_controller_test.go +++ b/integrations/operator/controllers/resources/openssheiceserverv2_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -48,6 +49,7 @@ var opensshEICEServerV2Spec = types.ServerSpecV2{ type opensshEICEServerV2TestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithLabelsAdapter[types.Server] } func (g *opensshEICEServerV2TestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/opensshserverv2_controller.go b/integrations/operator/controllers/resources/opensshserverv2_controller.go index 199ac67a42a7b..bef2104c2d316 100644 --- a/integrations/operator/controllers/resources/opensshserverv2_controller.go +++ b/integrations/operator/controllers/resources/opensshserverv2_controller.go @@ -28,6 +28,8 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // openSSHServerClient implements TeleportResourceClient and offers CRUD methods @@ -71,12 +73,12 @@ func (r openSSHServerClient) Delete(ctx context.Context, name string) error { // NewOpenSSHServerV2Reconciler instantiates a new Kubernetes controller // reconciling OpenSSH server resources. -func NewOpenSSHServerV2Reconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewOpenSSHServerV2Reconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { serverClient := &openSSHServerClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.Server, *resourcesv1.TeleportOpenSSHServerV2]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.Server, *resourcesv1.TeleportOpenSSHServerV2]( client, serverClient, ) diff --git a/integrations/operator/controllers/resources/opensshserverv2_controller_test.go b/integrations/operator/controllers/resources/opensshserverv2_controller_test.go index bf5731a7c01b3..fc0c353ce8e23 100644 --- a/integrations/operator/controllers/resources/opensshserverv2_controller_test.go +++ b/integrations/operator/controllers/resources/opensshserverv2_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -40,6 +41,7 @@ var opensshServerV2Spec = types.ServerSpecV2{ type opensshServerV2TestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithLabelsAdapter[types.Server] } func (g *opensshServerV2TestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/provision_token_controller.go b/integrations/operator/controllers/resources/provision_token_controller.go index 3067dc50e74aa..4027966e7c88e 100644 --- a/integrations/operator/controllers/resources/provision_token_controller.go +++ b/integrations/operator/controllers/resources/provision_token_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // provisionTokenClient implements TeleportResourceClient and offers CRUD methods needed to reconcile provision tokens @@ -56,12 +58,12 @@ func (r provisionTokenClient) Delete(ctx context.Context, name string) error { } // NewProvisionTokenReconciler instantiates a new Kubernetes controller reconciling provision token resources -func NewProvisionTokenReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewProvisionTokenReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { tokenClient := &provisionTokenClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.ProvisionToken, *resourcesv2.TeleportProvisionToken]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[types.ProvisionToken, *resourcesv2.TeleportProvisionToken]( client, tokenClient, ) diff --git a/integrations/operator/controllers/resources/provision_token_controller_test.go b/integrations/operator/controllers/resources/provision_token_controller_test.go index 9f5f781e6c40e..18f5cb2c49c1a 100644 --- a/integrations/operator/controllers/resources/provision_token_controller_test.go +++ b/integrations/operator/controllers/resources/provision_token_controller_test.go @@ -32,7 +32,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" - "github.com/gravitational/teleport/integrations/operator/controllers/resources" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -69,6 +69,7 @@ func newProvisionTokenFromSpecNoExpire(token string, spec types.ProvisionTokenSp type tokenTestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithoutLabelsAdapter[types.ProvisionToken] } func (g *tokenTestingPrimitives) Init(setup *testSetup) { @@ -197,7 +198,7 @@ github: tokenName := validRandomResourceName("token-") - obj, err := resources.GetUnstructuredObjectFromGVK(teleportTokenGVK) + obj, err := reconcilers.GetUnstructuredObjectFromGVK(teleportTokenGVK) require.NoError(t, err) obj.Object["spec"] = tokenManifest obj.SetName(tokenName) diff --git a/integrations/operator/controllers/resources/role_controller_test.go b/integrations/operator/controllers/resources/role_controller_test.go index ce8f247f3d8ec..58c7da48235a7 100644 --- a/integrations/operator/controllers/resources/role_controller_test.go +++ b/integrations/operator/controllers/resources/role_controller_test.go @@ -38,7 +38,7 @@ import ( apiutils "github.com/gravitational/teleport/api/utils" apiresources "github.com/gravitational/teleport/integrations/operator/apis/resources" resourcesv5 "github.com/gravitational/teleport/integrations/operator/apis/resources/v5" - "github.com/gravitational/teleport/integrations/operator/controllers/resources" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) var TeleportRoleGVKV5 = schema.GroupVersionKind{ @@ -198,7 +198,7 @@ allow: roleName := validRandomResourceName("role-") - obj, err := resources.GetUnstructuredObjectFromGVK(TeleportRoleGVKV5) + obj, err := reconcilers.GetUnstructuredObjectFromGVK(TeleportRoleGVKV5) require.NoError(t, err) obj.Object["spec"] = roleManifest obj.SetName(roleName) diff --git a/integrations/operator/controllers/resources/rolev6_controller_test.go b/integrations/operator/controllers/resources/rolev6_controller_test.go index 66341c4f84e79..35b913cbbb243 100644 --- a/integrations/operator/controllers/resources/rolev6_controller_test.go +++ b/integrations/operator/controllers/resources/rolev6_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -53,6 +54,7 @@ var roleV6Spec = types.RoleSpecV6{ type roleV6TestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithLabelsAdapter[types.Role] } func (g *roleV6TestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/rolev7_controller_test.go b/integrations/operator/controllers/resources/rolev7_controller_test.go index 1091680a75485..c798a00f666d3 100644 --- a/integrations/operator/controllers/resources/rolev7_controller_test.go +++ b/integrations/operator/controllers/resources/rolev7_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -50,6 +51,7 @@ var roleV7Spec = types.RoleSpecV6{ type roleV7TestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithLabelsAdapter[types.Role] } func (g *roleV7TestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/rolevX_controller.go b/integrations/operator/controllers/resources/rolevX_controller.go index c4ceaf7ec7903..bfc2d31f9d177 100644 --- a/integrations/operator/controllers/resources/rolevX_controller.go +++ b/integrations/operator/controllers/resources/rolevX_controller.go @@ -28,6 +28,8 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" resourcesv5 "github.com/gravitational/teleport/integrations/operator/apis/resources/v5" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // roleClient implements TeleportResourceClient and offers CRUD methods needed to reconcile roles @@ -62,12 +64,12 @@ func (r roleClient) Delete(ctx context.Context, name string) error { } // NewRoleReconciler instantiates a new Kubernetes controller reconciling legacy role v5 resources -func NewRoleReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewRoleReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { roleClient := &roleClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.Role, *resourcesv5.TeleportRole]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.Role, *resourcesv5.TeleportRole]( client, roleClient, ) @@ -76,12 +78,12 @@ func NewRoleReconciler(client kclient.Client, tClient *client.Client) (Reconcile } // NewRoleV6Reconciler instantiates a new Kubernetes controller reconciling role v6 resources -func NewRoleV6Reconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewRoleV6Reconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { roleClient := &roleClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.Role, *resourcesv1.TeleportRoleV6]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.Role, *resourcesv1.TeleportRoleV6]( client, roleClient, ) @@ -90,12 +92,12 @@ func NewRoleV6Reconciler(client kclient.Client, tClient *client.Client) (Reconci } // NewRoleV7Reconciler instantiates a new Kubernetes controller reconciling role v7 resources -func NewRoleV7Reconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewRoleV7Reconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { roleClient := &roleClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.Role, *resourcesv1.TeleportRoleV7]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.Role, *resourcesv1.TeleportRoleV7]( client, roleClient, ) diff --git a/integrations/operator/controllers/resources/saml_connector_controller.go b/integrations/operator/controllers/resources/saml_connector_controller.go index e940fcec91340..c39bfdf41b522 100644 --- a/integrations/operator/controllers/resources/saml_connector_controller.go +++ b/integrations/operator/controllers/resources/saml_connector_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // samlConnectorClient implements TeleportResourceClient and offers CRUD methods needed to reconcile saml_connectors @@ -58,12 +60,12 @@ func (r samlConnectorClient) Delete(ctx context.Context, name string) error { } // NewSAMLConnectorReconciler instantiates a new Kubernetes controller reconciling saml_connector resources -func NewSAMLConnectorReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewSAMLConnectorReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { samlClient := &samlConnectorClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.SAMLConnector, *resourcesv2.TeleportSAMLConnector]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithoutLabelsReconciler[types.SAMLConnector, *resourcesv2.TeleportSAMLConnector]( client, samlClient, ) diff --git a/integrations/operator/controllers/resources/saml_connector_controller_test.go b/integrations/operator/controllers/resources/saml_connector_controller_test.go index fbb3c3f921982..7b06111a2e89e 100644 --- a/integrations/operator/controllers/resources/saml_connector_controller_test.go +++ b/integrations/operator/controllers/resources/saml_connector_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/types" resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -48,6 +49,7 @@ var samlSpec = &types.SAMLConnectorSpecV2{ type samlTestingPrimitives struct { setup *testSetup + reconcilers.ResourceWithoutLabelsAdapter[types.SAMLConnector] } func (g *samlTestingPrimitives) Init(setup *testSetup) { diff --git a/integrations/operator/controllers/resources/global.go b/integrations/operator/controllers/resources/setup.go similarity index 67% rename from integrations/operator/controllers/resources/global.go rename to integrations/operator/controllers/resources/setup.go index 7b3c5f09ddc34..174206857ffa9 100644 --- a/integrations/operator/controllers/resources/global.go +++ b/integrations/operator/controllers/resources/setup.go @@ -21,51 +21,20 @@ package resources import ( "github.com/go-logr/logr" "github.com/gravitational/trace" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" kclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" - resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" - resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" - resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" - resourcesv5 "github.com/gravitational/teleport/integrations/operator/apis/resources/v5" + "github.com/gravitational/teleport/integrations/operator/controllers" ) -// Scheme is a singleton scheme for all controllers -var Scheme = runtime.NewScheme() - -func init() { - utilruntime.Must(resourcesv5.AddToScheme(Scheme)) - utilruntime.Must(resourcesv3.AddToScheme(Scheme)) - utilruntime.Must(resourcesv2.AddToScheme(Scheme)) - utilruntime.Must(resourcesv1.AddToScheme(Scheme)) - - // Not needed to reconcile the teleport CRs, but needed for the controller manager. - // We are not doing something very kubernetes friendly, but it's easier to have a single - // scheme rather than having to build and propagate schemes in multiple places, which - // is error-prone and can lead to inconsistencies. - utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) - utilruntime.Must(apiextv1.AddToScheme(Scheme)) -} - type reconcilerFactory struct { cr string - factory func(kclient.Client, *client.Client) (Reconciler, error) -} - -// Reconciler extends the reconcile.Reconciler interface by adding a -// SetupWithManager function that creates a controller in the given manager. -type Reconciler interface { - reconcile.Reconciler - SetupWithManager(mgr manager.Manager) error + factory func(kclient.Client, *client.Client) (controllers.Reconciler, error) } +// SetupAllControllers sets up all controllers func SetupAllControllers(log logr.Logger, mgr manager.Manager, teleportClient *client.Client, features *proto.Features) error { reconcilers := []reconcilerFactory{ {"TeleportRole", NewRoleReconciler}, diff --git a/integrations/operator/controllers/resources/teleport_reconciler.go b/integrations/operator/controllers/resources/teleport_reconciler.go deleted file mode 100644 index bcc83a768ffe2..0000000000000 --- a/integrations/operator/controllers/resources/teleport_reconciler.go +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package resources - -import ( - "context" - "fmt" - "reflect" - "slices" - - "github.com/gravitational/trace" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - ctrl "sigs.k8s.io/controller-runtime" - kclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - "github.com/gravitational/teleport/api/types" -) - -type TeleportResource interface { - GetName() string - SetOrigin(string) - GetMetadata() types.Metadata - GetRevision() string - SetRevision(string) -} - -// TeleportKubernetesResource is a Kubernetes resource representing a Teleport resource -type TeleportKubernetesResource[T TeleportResource] interface { - kclient.Object - ToTeleport() T - StatusConditions() *[]v1.Condition -} - -// TeleportResourceReconciler is a Teleport generic reconciler. It reconciles TeleportKubernetesResource -// with Teleport's types.ResourceWithOrigin -type TeleportResourceReconciler[T TeleportResource, K TeleportKubernetesResource[T]] struct { - ResourceBaseReconciler - resourceClient TeleportResourceClient[T] - gvk schema.GroupVersionKind -} - -// TeleportResourceClient is a CRUD client for a specific Teleport resource. -// Implementing this interface allows to be reconciled by the TeleportResourceReconciler -// instead of writing a new specific reconciliation loop. -// TeleportResourceClient implementations can optionally implement TeleportResourceMutator -type TeleportResourceClient[T TeleportResource] interface { - Get(context.Context, string) (T, error) - Create(context.Context, T) error - Update(context.Context, T) error - Delete(context.Context, string) error -} - -// TeleportResourceMutator can be implemented by TeleportResourceClients -// to edit a resource before its creation/update. -type TeleportResourceMutator[T TeleportResource] interface { - Mutate(new T) -} - -// TeleportExistingResourceMutator can be implemented by TeleportResourceClients -// to edit a resource before its update based on the existing one. -type TeleportExistingResourceMutator[T TeleportResource] interface { - MutateExisting(new, existing T) -} - -// NewTeleportResourceReconciler instanciates a TeleportResourceReconciler from a TeleportResourceClient. -func NewTeleportResourceReconciler[T TeleportResource, K TeleportKubernetesResource[T]]( - client kclient.Client, - resourceClient TeleportResourceClient[T], -) (*TeleportResourceReconciler[T, K], error) { - gvk, err := gvkFromScheme[T, K](Scheme) - if err != nil { - return nil, trace.Wrap(err) - } - reconciler := &TeleportResourceReconciler[T, K]{ - ResourceBaseReconciler: ResourceBaseReconciler{Client: client}, - resourceClient: resourceClient, - gvk: gvk, - } - reconciler.ResourceBaseReconciler.UpsertExternal = reconciler.Upsert - reconciler.ResourceBaseReconciler.DeleteExternal = reconciler.Delete - return reconciler, nil -} - -// Upsert is the TeleportResourceReconciler of the ResourceBaseReconciler UpsertExternal -// It contains the logic to check if the resource already exists, if it is owned by the operator and what -// to do to reconcile the Teleport resource based on the Kubernetes one. -func (r TeleportResourceReconciler[T, K]) Upsert(ctx context.Context, obj kclient.Object) error { - u, ok := obj.(*unstructured.Unstructured) - if !ok { - return fmt.Errorf("failed to convert Object into resource object: %T", obj) - } - k8sResource := newKubeResource[T, K]() - - // If an error happens we want to put it in status.conditions before returning. - err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation( - u.Object, - k8sResource, - true, /* returnUnknownFields */ - ) - updateErr := updateStatus(updateStatusConfig{ - ctx: ctx, - client: r.Client, - k8sResource: k8sResource, - condition: getStructureConditionFromError(err), - }) - if err != nil || updateErr != nil { - return trace.NewAggregate(err, updateErr) - } - - teleportResource := k8sResource.ToTeleport() - - existingResource, err := r.resourceClient.Get(ctx, teleportResource.GetName()) - updateErr = updateStatus(updateStatusConfig{ - ctx: ctx, - client: r.Client, - k8sResource: k8sResource, - condition: getReconciliationConditionFromError(err, true /* ignoreNotFound */), - }) - - if err != nil && !trace.IsNotFound(err) || updateErr != nil { - return trace.NewAggregate(err, updateErr) - } - // If err is nil, we found the resource. If err != nil (and we did return), then the error was `NotFound` - exists := err == nil - - if exists { - newOwnershipCondition, isOwned := checkOwnership(existingResource) - if updateErr = updateStatus(updateStatusConfig{ - ctx: ctx, - client: r.Client, - k8sResource: k8sResource, - condition: newOwnershipCondition, - }); updateErr != nil { - return trace.Wrap(updateErr) - } - if !isOwned { - return trace.AlreadyExists("unowned resource '%s' already exists", existingResource.GetName()) - } - } else { - if updateErr = updateStatus(updateStatusConfig{ - ctx: ctx, - client: r.Client, - k8sResource: k8sResource, - condition: newResourceCondition, - }); updateErr != nil { - return trace.Wrap(updateErr) - } - } - - teleportResource.SetOrigin(types.OriginKubernetes) - - if !exists { - // This is a new resource - if mutator, ok := r.resourceClient.(TeleportResourceMutator[T]); ok { - mutator.Mutate(teleportResource) - } - - err = r.resourceClient.Create(ctx, teleportResource) - } else { - // This is a resource update, we must propagate the revision - teleportResource.SetRevision(existingResource.GetRevision()) - if mutator, ok := r.resourceClient.(TeleportExistingResourceMutator[T]); ok { - mutator.MutateExisting(teleportResource, existingResource) - } - - err = r.resourceClient.Update(ctx, teleportResource) - } - // If an error happens we want to put it in status.conditions before returning. - updateErr = updateStatus(updateStatusConfig{ - ctx: ctx, - client: r.Client, - k8sResource: k8sResource, - condition: getReconciliationConditionFromError(err, false /* ignoreNotFound */), - }) - - return trace.NewAggregate(err, updateErr) -} - -// Delete is the TeleportResourceReconciler of the ResourceBaseReconciler DeleteExertal -func (r TeleportResourceReconciler[T, K]) Delete(ctx context.Context, obj kclient.Object) error { - // This call catches non-existing resources or subkind mismatch (e.g. openssh nodes) - // We can then check that we own the resource before deleting it. - resource, err := r.resourceClient.Get(ctx, obj.GetName()) - if err != nil { - return trace.Wrap(err) - } - - _, isOwned := checkOwnership(resource) - if !isOwned { - // The resource doesn't belong to us, we bail out but unblock the CR deletion - return nil - } - // This GET->check->DELETE dance is race-prone, but it's good enough for what - // we want to do. No one should reconcile the same resource as the operator. - // If they do, it's their fault as the resource was clearly flagged as belonging to us. - return r.resourceClient.Delete(ctx, obj.GetName()) -} - -// Reconcile allows the TeleportResourceReconciler to implement the reconcile.Reconciler interface -func (r TeleportResourceReconciler[T, K]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - obj, err := GetUnstructuredObjectFromGVK(r.gvk) - if err != nil { - return ctrl.Result{}, trace.Wrap(err, "creating object in which the CR will be unmarshalled") - } - return r.Do(ctx, req, obj) -} - -// SetupWithManager have a controllerruntime.Manager run the TeleportResourceReconciler -func (r TeleportResourceReconciler[T, K]) SetupWithManager(mgr ctrl.Manager) error { - // The TeleportResourceReconciler uses unstructured objects because of a silly json marshaling - // issue. Teleport's utils.String is a list of strings, but marshals as a single string if there's a single item. - // This is a questionable design as it breaks the openapi schema, but we're stuck with it. We had to relax openapi - // validation in those CRD fields, and use an unstructured object for the client, else JSON unmarshalling fails. - obj, err := GetUnstructuredObjectFromGVK(r.gvk) - if err != nil { - return trace.Wrap(err, "creating the model object for the manager watcher/client") - } - return ctrl. - NewControllerManagedBy(mgr). - For(obj). - WithEventFilter( - buildPredicate(), - ). - Complete(r) -} - -// gvkFromScheme looks up the GVK from the a runtime scheme. -// The structured type must have been registered before in the scheme. This function is used when you have a structured -// type, a scheme containing this structured type, and want to build an unstructured object for the same GVK. -func gvkFromScheme[T TeleportResource, K TeleportKubernetesResource[T]](scheme *runtime.Scheme) (schema.GroupVersionKind, error) { - structuredObj := newKubeResource[T, K]() - gvks, _, err := scheme.ObjectKinds(structuredObj) - if err != nil { - return schema.GroupVersionKind{}, trace.Wrap(err, "looking up gvk in scheme for type %T", structuredObj) - } - if len(gvks) != 1 { - return schema.GroupVersionKind{}, trace.CompareFailed( - "failed GVK lookup in scheme, looked up %T and got %d matches, expected 1", structuredObj, len(gvks), - ) - } - return gvks[0], nil -} - -// newKubeResource creates a new TeleportKubernetesResource -// the function supports structs or pointer to struct implementations of the TeleportKubernetesResource interface -func newKubeResource[T TeleportResource, K TeleportKubernetesResource[T]]() K { - // We create a new instance of K. - var resource K - // We take the type of K - interfaceType := reflect.TypeOf(resource) - // If K is not a pointer we don't need to do anything - // If K is a pointer, new(K) is only initializing a nil pointer, we need to manually initialize its destination - if interfaceType.Kind() == reflect.Ptr { - // We create a new Value of the type pointed by K. reflect.New returns a pointer to this value - initializedResource := reflect.New(interfaceType.Elem()) - // We cast back to K - resource = initializedResource.Interface().(K) - } - return resource -} - -// buildPredicate returns a predicate that triggers the reconciliation when: -// - the resource generation changes -// - the resource finalizers change -// - the resource annotations change -// - the resource labels change -// - the resource is created -// - the resource is deleted -// It does not trigger the reconciliation when: -// - the resource status changes -func buildPredicate() predicate.Predicate { - return predicate.Or( - predicate.GenerationChangedPredicate{}, - predicate.AnnotationChangedPredicate{}, - predicate.LabelChangedPredicate{}, - predicate.Funcs{ - UpdateFunc: func(e event.UpdateEvent) bool { - if e.ObjectOld == nil || e.ObjectNew == nil { - return false - } - - return !slices.Equal(e.ObjectNew.GetFinalizers(), e.ObjectOld.GetFinalizers()) - }, - }, - ) -} diff --git a/integrations/operator/controllers/resources/teleport_reconciler_test.go b/integrations/operator/controllers/resources/teleport_reconciler_test.go deleted file mode 100644 index ef7bd8caefeb1..0000000000000 --- a/integrations/operator/controllers/resources/teleport_reconciler_test.go +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package resources - -import ( - "context" - "testing" - - "github.com/google/uuid" - "github.com/gravitational/trace" - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - kclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/integrations/operator/apis/resources" -) - -// NewFakeTeleportResource creates a FakeTeleportResource -func NewFakeTeleportResource(metadata types.Metadata) *FakeTeleportResource { - return &FakeTeleportResource{metadata: metadata} -} - -// FakeTeleportResource implements the TeleportResource interface for testing purposes. -// Its corresponding TeleportKubernetesResource is FakeTeleportKubernetesResource. -type FakeTeleportResource struct { - metadata types.Metadata -} - -// GetName implements TeleportResource interface. -func (f *FakeTeleportResource) GetName() string { - return f.metadata.Name -} - -// SetOrigin implements TeleportResource interface. -func (f *FakeTeleportResource) SetOrigin(origin string) { - f.metadata.Labels[types.OriginLabel] = origin -} - -// GetMetadata implements TeleportResource interface. -func (f *FakeTeleportResource) GetMetadata() types.Metadata { - return f.metadata -} - -// GetRevision implements TeleportResource interface. -func (f *FakeTeleportResource) GetRevision() string { - return f.metadata.Revision -} - -// SetRevision implements TeleportResource interface. -func (f *FakeTeleportResource) SetRevision(revision string) { - f.metadata.Revision = revision -} - -// FakeTeleportResourceClient implements the TeleportResourceClient interface -// for the FakeTeleportResource. It mimics a teleport server by tracking the -// resource state in its store. -type FakeTeleportResourceClient struct { - store map[string]types.Metadata -} - -// Get implements the TeleportResourceClient interface. -func (f *FakeTeleportResourceClient) Get(_ context.Context, name string) (*FakeTeleportResource, error) { - metadata, ok := f.store[name] - if !ok { - return nil, trace.NotFound("%q not found", name) - } - return NewFakeTeleportResource(metadata), nil -} - -// Create implements the TeleportResourceClient interface. -func (f *FakeTeleportResourceClient) Create(_ context.Context, t *FakeTeleportResource) error { - _, ok := f.store[t.GetName()] - if ok { - return trace.AlreadyExists("%q already exists", t.GetName()) - } - metadata := t.GetMetadata() - metadata.SetRevision(uuid.New().String()) - f.store[t.GetName()] = metadata - return nil -} - -// Update implements the TeleportResourceClient interface. -func (f *FakeTeleportResourceClient) Update(_ context.Context, t *FakeTeleportResource) error { - existing, ok := f.store[t.GetName()] - if !ok { - return trace.NotFound("%q not found", t.GetName()) - } - if existing.Revision != t.GetRevision() { - return trace.CompareFailed("revision mismatch") - } - metadata := t.GetMetadata() - metadata.SetRevision(uuid.New().String()) - f.store[t.GetName()] = metadata - return nil -} - -// Delete implements the TeleportResourceClient interface. -func (f *FakeTeleportResourceClient) Delete(_ context.Context, name string) error { - _, ok := f.store[name] - if !ok { - return trace.NotFound("%q not found", name) - } - delete(f.store, name) - return nil - -} - -// resourceExists checks if a resource is in the store. -// This is use fr testing purposes. -func (f *FakeTeleportResourceClient) resourceExists(name string) bool { - _, ok := f.store[name] - return ok -} - -// FakeTeleportKubernetesResource implements the TeleportKubernetesResource -// interface for testing purposes. -// Its corresponding TeleportResource is FakeTeleportResource. -type FakeTeleportKubernetesResource struct { - kclient.Object - status resources.Status -} - -// ToTeleport implements the TeleportKubernetesResource interface. -func (f *FakeTeleportKubernetesResource) ToTeleport() *FakeTeleportResource { - return &FakeTeleportResource{ - metadata: types.Metadata{ - Name: f.GetName(), - Namespace: defaults.Namespace, - Labels: map[string]string{}, - }, - } -} - -// StatusConditions implements the TeleportKubernetesResource interface. -func (f *FakeTeleportKubernetesResource) StatusConditions() *[]metav1.Condition { - return &f.status.Conditions -} - -func TestTeleportResourceReconciler_Delete(t *testing.T) { - ctx := context.Background() - resourceName := "test" - kubeResource := &unstructured.Unstructured{} - kubeResource.SetName(resourceName) - - tests := []struct { - name string - store map[string]types.Metadata - assertErr require.ErrorAssertionFunc - resourceExists bool - }{ - { - name: "delete non-existing resource", - store: map[string]types.Metadata{}, - assertErr: func(t require.TestingT, err error, i ...interface{}) { - require.True(t, trace.IsNotFound(err)) - }, - resourceExists: false, - }, - { - name: "delete existing resource", - store: map[string]types.Metadata{ - resourceName: { - Name: resourceName, - Labels: map[string]string{types.OriginLabel: types.OriginKubernetes}, - }, - }, - assertErr: require.NoError, - resourceExists: false, - }, - { - name: "delete existing but not owned resource", - store: map[string]types.Metadata{ - resourceName: { - Name: resourceName, - Labels: map[string]string{}, - }, - }, - assertErr: require.NoError, - resourceExists: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resourceClient := &FakeTeleportResourceClient{tt.store} - reconciler := TeleportResourceReconciler[*FakeTeleportResource, *FakeTeleportKubernetesResource]{ - resourceClient: resourceClient, - } - tt.assertErr(t, reconciler.Delete(ctx, kubeResource)) - require.Equal(t, tt.resourceExists, resourceClient.resourceExists(resourceName)) - }) - } -} diff --git a/integrations/operator/controllers/resources/testlib/accesslist_controller_tests.go b/integrations/operator/controllers/resources/testlib/accesslist_controller_tests.go index fef9d8e803234..29c8082825298 100644 --- a/integrations/operator/controllers/resources/testlib/accesslist_controller_tests.go +++ b/integrations/operator/controllers/resources/testlib/accesslist_controller_tests.go @@ -36,6 +36,7 @@ import ( "github.com/gravitational/teleport/api/types/header" "github.com/gravitational/teleport/api/types/trait" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources" ) @@ -70,6 +71,7 @@ func newAccessListSpec(nextAudit time.Time) accesslist.Spec { type accessListTestingPrimitives struct { setup *TestSetup + reconcilers.ResourceWithLabelsAdapter[*accesslist.AccessList] } func (g *accessListTestingPrimitives) Init(setup *TestSetup) { diff --git a/integrations/operator/controllers/resources/testlib/env.go b/integrations/operator/controllers/resources/testlib/env.go index be6033a872290..4f0dc33470c85 100644 --- a/integrations/operator/controllers/resources/testlib/env.go +++ b/integrations/operator/controllers/resources/testlib/env.go @@ -51,6 +51,7 @@ import ( resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" resourcesv5 "github.com/gravitational/teleport/integrations/operator/apis/resources/v5" + "github.com/gravitational/teleport/integrations/operator/controllers" "github.com/gravitational/teleport/integrations/operator/controllers/resources" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/service/servicecfg" @@ -58,7 +59,7 @@ import ( // scheme is our own test-specific scheme to avoid using the global // unprotected scheme.Scheme that triggers the race detector -var scheme = resources.Scheme +var scheme = controllers.Scheme func init() { utilruntime.Must(core.AddToScheme(scheme)) diff --git a/integrations/operator/controllers/resources/testlib/login_rule_controller_tests.go b/integrations/operator/controllers/resources/testlib/login_rule_controller_tests.go index 88fcd429f4195..5bb8fc4544665 100644 --- a/integrations/operator/controllers/resources/testlib/login_rule_controller_tests.go +++ b/integrations/operator/controllers/resources/testlib/login_rule_controller_tests.go @@ -34,10 +34,12 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) type loginRuleTestingPrimitives struct { setup *TestSetup + reconcilers.ResourceWithoutLabelsAdapter[*resourcesv1.LoginRuleResource] } func (l *loginRuleTestingPrimitives) Init(setup *TestSetup) { diff --git a/integrations/operator/controllers/resources/testlib/teleport_reconciler_tests.go b/integrations/operator/controllers/resources/testlib/teleport_reconciler_tests.go index 090ce955ac053..ebc244a8d574a 100644 --- a/integrations/operator/controllers/resources/testlib/teleport_reconciler_tests.go +++ b/integrations/operator/controllers/resources/testlib/teleport_reconciler_tests.go @@ -29,10 +29,13 @@ import ( "k8s.io/client-go/util/retry" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/integrations/operator/controllers/resources" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) -type ResourceTestingPrimitives[T resources.TeleportResource, K resources.TeleportKubernetesResource[T]] interface { +type ResourceTestingPrimitives[T reconcilers.Resource, K reconcilers.KubernetesCR[T]] interface { + // Adapter allows to recover the name revision and labels of a resource + reconcilers.Adapter[T] + // Setup the testing suite Init(setup *TestSetup) SetupTeleportFixtures(context.Context) error // Interacting with the Teleport Resource @@ -48,7 +51,7 @@ type ResourceTestingPrimitives[T resources.TeleportResource, K resources.Telepor CompareTeleportAndKubernetesResource(T, K) (bool, string) } -func ResourceCreationTest[T resources.TeleportResource, K resources.TeleportKubernetesResource[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { +func ResourceCreationTest[T reconcilers.Resource, K reconcilers.KubernetesCR[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { ctx := context.Background() setup := SetupTestEnv(t, opts...) test.Init(setup) @@ -66,9 +69,8 @@ func ResourceCreationTest[T resources.TeleportResource, K resources.TeleportKube return !trace.IsNotFound(err) }) require.NoError(t, err) - require.Equal(t, resourceName, tResource.GetName()) - require.Contains(t, tResource.GetMetadata().Labels, types.OriginLabel) - require.Equal(t, types.OriginKubernetes, tResource.GetMetadata().Labels[types.OriginLabel]) + require.Equal(t, resourceName, test.GetResourceName(tResource)) + require.Equal(t, types.OriginKubernetes, test.GetResourceOrigin(tResource)) err = test.DeleteKubernetesResource(ctx, resourceName) require.NoError(t, err) @@ -79,7 +81,7 @@ func ResourceCreationTest[T resources.TeleportResource, K resources.TeleportKube }) } -func ResourceDeletionDriftTest[T resources.TeleportResource, K resources.TeleportKubernetesResource[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { +func ResourceDeletionDriftTest[T reconcilers.Resource, K reconcilers.KubernetesCR[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { ctx := context.Background() setup := SetupTestEnv(t, opts...) test.Init(setup) @@ -98,10 +100,9 @@ func ResourceDeletionDriftTest[T resources.TeleportResource, K resources.Telepor }) require.NoError(t, err) - require.Equal(t, resourceName, tResource.GetName()) + require.Equal(t, resourceName, test.GetResourceName(tResource)) - require.Contains(t, tResource.GetMetadata().Labels, types.OriginLabel) - require.Equal(t, types.OriginKubernetes, tResource.GetMetadata().Labels[types.OriginLabel]) + require.Equal(t, types.OriginKubernetes, test.GetResourceOrigin(tResource)) // We cause a drift by altering the Teleport resource. // To make sure the operator does not reconcile while we're finished we suspend the operator @@ -128,7 +129,7 @@ func ResourceDeletionDriftTest[T resources.TeleportResource, K resources.Telepor }) } -func ResourceUpdateTest[T resources.TeleportResource, K resources.TeleportKubernetesResource[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { +func ResourceUpdateTest[T reconcilers.Resource, K reconcilers.KubernetesCR[T]](t *testing.T, test ResourceTestingPrimitives[T, K], opts ...TestOption) { ctx := context.Background() setup := SetupTestEnv(t, opts...) test.Init(setup) diff --git a/integrations/operator/controllers/resources/user_controller.go b/integrations/operator/controllers/resources/user_controller.go index d446e1a3c3eaa..83962014cd9c6 100644 --- a/integrations/operator/controllers/resources/user_controller.go +++ b/integrations/operator/controllers/resources/user_controller.go @@ -27,6 +27,8 @@ import ( "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/types" resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" + "github.com/gravitational/teleport/integrations/operator/controllers" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" ) // userClient implements TeleportResourceClient and offers CRUD methods needed to reconcile users @@ -65,12 +67,12 @@ func (r userClient) MutateExisting(newUser, existingUser types.User) { } // NewUserReconciler instantiates a new Kubernetes controller reconciling user resources -func NewUserReconciler(client kclient.Client, tClient *client.Client) (Reconciler, error) { +func NewUserReconciler(client kclient.Client, tClient *client.Client) (controllers.Reconciler, error) { userClient := &userClient{ teleportClient: tClient, } - resourceReconciler, err := NewTeleportResourceReconciler[types.User, *resourcesv2.TeleportUser]( + resourceReconciler, err := reconcilers.NewTeleportResourceWithLabelsReconciler[types.User, *resourcesv2.TeleportUser]( client, userClient, ) diff --git a/integrations/operator/controllers/resources/user_controller_test.go b/integrations/operator/controllers/resources/user_controller_test.go index 38d56d3bf711a..373fcf7df29c3 100644 --- a/integrations/operator/controllers/resources/user_controller_test.go +++ b/integrations/operator/controllers/resources/user_controller_test.go @@ -39,7 +39,7 @@ import ( "github.com/gravitational/teleport/api/types" apiresources "github.com/gravitational/teleport/integrations/operator/apis/resources" v2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" - "github.com/gravitational/teleport/integrations/operator/controllers/resources" + "github.com/gravitational/teleport/integrations/operator/controllers/reconcilers" "github.com/gravitational/teleport/integrations/operator/controllers/resources/testlib" ) @@ -159,7 +159,7 @@ traits: userName := validRandomResourceName("user-") - obj, err := resources.GetUnstructuredObjectFromGVK(teleportUserGVK) + obj, err := reconcilers.GetUnstructuredObjectFromGVK(teleportUserGVK) require.NoError(t, err) obj.Object["spec"] = userManifest obj.SetName(userName) diff --git a/integrations/operator/controllers/scheme.go b/integrations/operator/controllers/scheme.go new file mode 100644 index 0000000000000..7279014fc0d1d --- /dev/null +++ b/integrations/operator/controllers/scheme.go @@ -0,0 +1,48 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package controllers + +import ( + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + resourcesv1 "github.com/gravitational/teleport/integrations/operator/apis/resources/v1" + resourcesv2 "github.com/gravitational/teleport/integrations/operator/apis/resources/v2" + resourcesv3 "github.com/gravitational/teleport/integrations/operator/apis/resources/v3" + resourcesv5 "github.com/gravitational/teleport/integrations/operator/apis/resources/v5" +) + +// Scheme is a singleton scheme for all controllers +var Scheme = runtime.NewScheme() + +func init() { + utilruntime.Must(resourcesv5.AddToScheme(Scheme)) + utilruntime.Must(resourcesv3.AddToScheme(Scheme)) + utilruntime.Must(resourcesv2.AddToScheme(Scheme)) + utilruntime.Must(resourcesv1.AddToScheme(Scheme)) + + // Not needed to reconcile the teleport CRs, but needed for the controller manager. + // We are not doing something very kubernetes friendly, but it's easier to have a single + // scheme rather than having to build and propagate schemes in multiple places, which + // is error-prone and can lead to inconsistencies. + utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) + utilruntime.Must(apiextv1.AddToScheme(Scheme)) +} diff --git a/integrations/operator/main.go b/integrations/operator/main.go index 7c81f8685ab20..ac6b485f8c8e9 100644 --- a/integrations/operator/main.go +++ b/integrations/operator/main.go @@ -34,12 +34,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "github.com/gravitational/teleport/integrations/operator/controllers" "github.com/gravitational/teleport/integrations/operator/controllers/resources" "github.com/gravitational/teleport/integrations/operator/embeddedtbot" ) var ( - scheme = resources.Scheme + scheme = controllers.Scheme setupLog = ctrl.Log.WithName("setup") )