diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d316feba7..3a9f89b09 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -47,6 +47,12 @@ rules: - patch - update - watch +- apiGroups: + - appstudio.redhat.com + resources: + - components/finalizers + verbs: + - update - apiGroups: - appstudio.redhat.com resources: diff --git a/controllers/component/component_adapter.go b/controllers/component/component_adapter.go new file mode 100644 index 000000000..bc822c55f --- /dev/null +++ b/controllers/component/component_adapter.go @@ -0,0 +1,132 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package component + +import ( + "context" + + applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" + "github.com/redhat-appstudio/integration-service/gitops" + h "github.com/redhat-appstudio/integration-service/helpers" + "github.com/redhat-appstudio/integration-service/loader" + "github.com/redhat-appstudio/integration-service/metrics" + "github.com/redhat-appstudio/operator-toolkit/controller" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Adapter holds the objects needed to reconcile a integration PipelineRun. +type Adapter struct { + component *applicationapiv1alpha1.Component + application *applicationapiv1alpha1.Application + loader loader.ObjectLoader + logger h.IntegrationLogger + client client.Client + context context.Context +} + +// NewAdapter creates and returns an Adapter instance. +func NewAdapter(component *applicationapiv1alpha1.Component, application *applicationapiv1alpha1.Application, logger h.IntegrationLogger, loader loader.ObjectLoader, client client.Client, + context context.Context) *Adapter { + return &Adapter{ + component: component, + application: application, + logger: logger, + loader: loader, + client: client, + context: context, + } +} + +// EnsureComponentIsCleanedUp is an operation that will ensure components +// marked for deletion have a snapshot created without said component +func (a *Adapter) EnsureComponentIsCleanedUp() (controller.OperationResult, error) { + if !hasComponentBeenDeleted(a.component) { + return controller.ContinueProcessing() + } + + applicationComponents, err := a.loader.GetAllApplicationComponents(a.client, a.context, a.application) + if err != nil { + a.logger.Error(err, "Failed to load application components") + return controller.RequeueWithError(err) + } + + var snapshotComponents []applicationapiv1alpha1.SnapshotComponent + + for _, individualComponent := range *applicationComponents { + component := individualComponent + if a.component.Name != component.Name { + containerImage := component.Spec.ContainerImage + componentSource := gitops.GetComponentSourceFromComponent(&component) + snapshotComponents = append(snapshotComponents, applicationapiv1alpha1.SnapshotComponent{ + Name: component.Name, + ContainerImage: containerImage, + Source: *componentSource, + }) + } + } + + if len(snapshotComponents) == 0 { + a.logger.Info("Application has no available snapshot components for snapshot creation") + return controller.StopProcessing() + } + + _, err = a.createUpdatedSnapshot(&snapshotComponents) + if err != nil { + a.logger.Error(err, "Failed to create new snapshot after component deletion") + return controller.RequeueWithError(err) + } + + return controller.ContinueProcessing() +} + +// createUpdatedSnapshot prepares a Snapshot for a given application and component(s). +// In case the Snapshot can't be created, an error will be returned. +func (a *Adapter) createUpdatedSnapshot(snapshotComponents *[]applicationapiv1alpha1.SnapshotComponent) (*applicationapiv1alpha1.Snapshot, error) { + snapshot := gitops.NewSnapshot(a.application, snapshotComponents) + if snapshot.Labels == nil { + snapshot.Labels = map[string]string{} + } + snapshotType := gitops.SnapshotCompositeType + if len(*snapshotComponents) == 1 { + snapshotType = gitops.SnapshotComponentType + } + snapshot.Labels[gitops.SnapshotTypeLabel] = snapshotType + + err := ctrl.SetControllerReference(a.application, snapshot, a.client.Scheme()) + if err != nil { + a.logger.Error(err, "Failed to set controller refrence") + return nil, err + } + + err = a.client.Create(a.context, snapshot) + if err != nil { + a.logger.Error(err, "Failed to create new snapshot on client") + return nil, err + } + + go metrics.RegisterNewSnapshot() + return snapshot, nil +} + +func hasComponentBeenDeleted(object client.Object) bool { + + if comp, ok := object.(*applicationapiv1alpha1.Component); ok { + return !comp.ObjectMeta.DeletionTimestamp.IsZero() + } + return false +} diff --git a/controllers/component/component_adapter_test.go b/controllers/component/component_adapter_test.go new file mode 100644 index 000000000..cdcbd1ec8 --- /dev/null +++ b/controllers/component/component_adapter_test.go @@ -0,0 +1,145 @@ +package component + +import ( + "bytes" + "reflect" + "time" + + "github.com/tonglil/buflogr" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" + "github.com/redhat-appstudio/integration-service/loader" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/redhat-appstudio/integration-service/helpers" + "k8s.io/apimachinery/pkg/api/errors" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Component Adapter", Ordered, func() { + var ( + adapter *Adapter + logger helpers.IntegrationLogger + + hasApp *applicationapiv1alpha1.Application + hasComp *applicationapiv1alpha1.Component + hasComp2 *applicationapiv1alpha1.Component + ) + const ( + SampleCommit = "a2ba645d50e471d5f084b" + SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" + sample_revision = "random-value" + SampleDigest = "sha256:841328df1b9f8c4087adbdcfec6cc99ac8308805dea83f6d415d6fb8d40227c1" + SampleImageWithoutDigest = "quay.io/redhat-appstudio/sample-image" + SampleImage = SampleImageWithoutDigest + "@" + SampleDigest + ) + + BeforeAll(func() { + + hasApp = &applicationapiv1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "application-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ApplicationSpec{ + DisplayName: "application-sample", + Description: "This is an example application", + }, + } + Expect(k8sClient.Create(ctx, hasApp)).Should(Succeed()) + + hasComp = &applicationapiv1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "component-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ComponentSpec{ + ComponentName: "component-sample-2", + Application: hasApp.Name, + ContainerImage: SampleImage, + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + URL: SampleRepoLink, + Revision: SampleCommit, + }, + }, + }, + }, + Status: applicationapiv1alpha1.ComponentStatus{ + LastBuiltCommit: "", + }, + } + Expect(k8sClient.Create(ctx, hasComp)).Should(Succeed()) + + hasComp2 = &applicationapiv1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "component-second-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ComponentSpec{ + ComponentName: "component-second-sample", + Application: "application-sample", + ContainerImage: SampleImage, + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + URL: SampleRepoLink, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComp2)).Should(Succeed()) + }) + + AfterAll(func() { + err := k8sClient.Delete(ctx, hasApp) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComp) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComp2) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + }) + + It("can create a new Adapter instance", func() { + Expect(reflect.TypeOf(NewAdapter(hasComp, hasApp, logger, loader.NewMockLoader(), k8sClient, ctx))).To(Equal(reflect.TypeOf(&Adapter{}))) + }) + It("ensures removing a component will result in a new snapshot being created", func() { + buf := bytes.Buffer{} + + log := helpers.IntegrationLogger{Logger: buflogr.NewWithBuffer(&buf)} + adapter = NewAdapter(hasComp, hasApp, log, loader.NewMockLoader(), k8sClient, ctx) + adapter.context = loader.GetMockedContext(ctx, []loader.MockData{ + { + ContextKey: loader.ApplicationContextKey, + Resource: hasApp, + }, + { + ContextKey: loader.ApplicationComponentsContextKey, + Resource: []applicationapiv1alpha1.Component{*hasComp, *hasComp2}, + }, + }) + snapshots := &applicationapiv1alpha1.SnapshotList{} + Eventually(func() bool { + k8sClient.List(ctx, snapshots, &client.ListOptions{Namespace: hasApp.Namespace}) + return len(snapshots.Items) == 0 + }, time.Second*20).Should(BeTrue()) + + now := metav1.NewTime(metav1.Now().Add(time.Second * 1)) + hasComp.SetDeletionTimestamp(&now) + + result, err := adapter.EnsureComponentIsCleanedUp() + + Eventually(func() bool { + k8sClient.List(ctx, snapshots, &client.ListOptions{Namespace: hasApp.Namespace}) + return !result.CancelRequest && len(snapshots.Items) == 1 && err == nil + }, time.Second*20).Should(BeTrue()) + }) + +}) diff --git a/controllers/component/component_controller.go b/controllers/component/component_controller.go new file mode 100644 index 000000000..c00028c0a --- /dev/null +++ b/controllers/component/component_controller.go @@ -0,0 +1,113 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions andF +limitations under the License. +*/ + +package component + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" + "github.com/redhat-appstudio/integration-service/helpers" + "github.com/redhat-appstudio/integration-service/loader" + "github.com/redhat-appstudio/operator-toolkit/controller" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// Reconciler reconciles a component object +type Reconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +// NewComponentReconciler creates and returns a Reconciler. +func NewComponentReconciler(client client.Client, logger *logr.Logger, scheme *runtime.Scheme) *Reconciler { + return &Reconciler{ + Client: client, + Log: logger.WithName("integration pipeline"), + Scheme: scheme, + } +} + +//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := helpers.IntegrationLogger{Logger: r.Log.WithValues("component", req.NamespacedName)} + loader := loader.NewLoader() + + component := &applicationapiv1alpha1.Component{} + err := r.Get(ctx, req.NamespacedName, component) + if err != nil { + logger.Error(err, "Failed to get component for", "req", req.NamespacedName) + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, err + } + + var application *applicationapiv1alpha1.Application + application, err = loader.GetApplicationFromComponent(r.Client, ctx, component) + if err != nil { + logger.Error(err, "Failed to get Application from the component", + "Component.Name", component.Name) + return ctrl.Result{}, err + } + + if application == nil { + err := fmt.Errorf("failed to get Application") + logger.Error(err, "reconcile cannot resolve application") + return ctrl.Result{}, err + } + logger = logger.WithApp(*application) + + adapter := NewAdapter(component, application, logger, loader, r.Client, ctx) + + return controller.ReconcileHandler([]controller.Operation{ + adapter.EnsureComponentIsCleanedUp, + }) +} + +// AdapterInterface is an interface defining all the operations that should be defined in an Integration adapter. +type AdapterInterface interface { + EnsureSnapshotIsFresh() (controller.OperationResult, error) +} + +// SetupController creates a new Component controller and adds it to the Manager. +func SetupController(manager ctrl.Manager, log *logr.Logger) error { + return setupControllerWithManager(manager, NewComponentReconciler(manager.GetClient(), log, manager.GetScheme())) + +} + +// setupControllerWithManager sets up the controller with the Manager which monitors Components and filters +// out status updates. +func setupControllerWithManager(manager ctrl.Manager, controller *Reconciler) error { + return ctrl.NewControllerManagedBy(manager). + For(&applicationapiv1alpha1.Component{}). + WithEventFilter(predicate.Or( + ComponentDeletedPredicate())). + Complete(controller) +} diff --git a/controllers/component/component_controller_test.go b/controllers/component/component_controller_test.go new file mode 100644 index 000000000..0b0898055 --- /dev/null +++ b/controllers/component/component_controller_test.go @@ -0,0 +1,153 @@ +package component + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientsetscheme "k8s.io/client-go/kubernetes/scheme" + klog "k8s.io/klog/v2" +) + +var _ = Describe("ComponentController", func() { + var ( + manager ctrl.Manager + componentReconciler *Reconciler + scheme runtime.Scheme + req ctrl.Request + hasApp *applicationapiv1alpha1.Application + hasComp *applicationapiv1alpha1.Component + ) + const ( + SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic" + ) + + BeforeEach(func() { + + applicationName := "application-sample" + + hasApp = &applicationapiv1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: applicationName, + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ApplicationSpec{ + DisplayName: "application-sample", + Description: "This is an example application", + }, + } + + Expect(k8sClient.Create(ctx, hasApp)).Should(Succeed()) + + hasComp = &applicationapiv1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "component-sample", + Namespace: "default", + }, + Spec: applicationapiv1alpha1.ComponentSpec{ + ComponentName: "component-sample", + Application: applicationName, + Source: applicationapiv1alpha1.ComponentSource{ + ComponentSourceUnion: applicationapiv1alpha1.ComponentSourceUnion{ + GitSource: &applicationapiv1alpha1.GitSource{ + URL: SampleRepoLink, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, hasComp)).Should(Succeed()) + + req = ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: hasComp.Name, + }, + } + + webhookInstallOptions := &testEnv.WebhookInstallOptions + + klog.Info(webhookInstallOptions.LocalServingHost) + klog.Info(webhookInstallOptions.LocalServingPort) + klog.Info(webhookInstallOptions.LocalServingCertDir) + + var err error + manager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: clientsetscheme.Scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + MetricsBindAddress: "0", // this disables metrics + LeaderElection: false, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(err).To(BeNil()) + + componentReconciler = NewComponentReconciler(k8sClient, &logf.Log, &scheme) + }) + AfterEach(func() { + err := k8sClient.Delete(ctx, hasApp) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + err = k8sClient.Delete(ctx, hasComp) + Expect(err == nil || errors.IsNotFound(err)).To(BeTrue()) + }) + + It("can create and return a new Reconciler object", func() { + Expect(reflect.TypeOf(componentReconciler)).To(Equal(reflect.TypeOf(&Reconciler{}))) + }) + + It("can fail when Reconcile fails to prepare the adapter when Component is not found", func() { + Expect(k8sClient.Delete(ctx, hasComp)).Should(Succeed()) + Eventually(func() error { + _, err := componentReconciler.Reconcile(ctx, req) + return err + }).Should(BeNil()) + }) + + It("can fail when Reconcile fails to prepare the adapter when Application is not found", func() { + Expect(k8sClient.Delete(ctx, hasApp)).Should(Succeed()) + Eventually(func() error { + _, err := componentReconciler.Reconcile(ctx, req) + return err + }).ShouldNot(BeNil()) + }) + + It("can Reconcile function prepare the adapter and return the result of the reconcile handling operation", func() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: "non-existent", + Namespace: "default", + }, + } + result, err := componentReconciler.Reconcile(ctx, req) + Expect(reflect.TypeOf(result)).To(Equal(reflect.TypeOf(reconcile.Result{}))) + Expect(err).To(BeNil()) + }) + + It("can setup a new controller manager with the given componentReconciler", func() { + err := setupControllerWithManager(manager, componentReconciler) + Expect(err).NotTo(HaveOccurred()) + }) + + It("can setup a new Controller manager and start it", func() { + err := SetupController(manager, &ctrl.Log) + Expect(err).To(BeNil()) + go func() { + defer GinkgoRecover() + err = manager.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + }) + +}) diff --git a/controllers/component/component_suite_test.go b/controllers/component/component_suite_test.go new file mode 100644 index 000000000..d75049a9f --- /dev/null +++ b/controllers/component/component_suite_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package component + +import ( + "context" + "go/build" + "path/filepath" + "testing" + + toolkit "github.com/redhat-appstudio/operator-toolkit/test" + + "k8s.io/client-go/rest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + ctrl "sigs.k8s.io/controller-runtime" + + applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" + "github.com/redhat-appstudio/integration-service/api/v1beta1" + releasev1alpha1 "github.com/redhat-appstudio/release-service/api/v1alpha1" + tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + clientsetscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) + +func TestControllerComponent(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Component Controller Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + ctx, cancel = context.WithCancel(context.TODO()) + + //adding required CRDs, including tekton for PipelineRun Kind + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "crd", "bases"), + filepath.Join( + build.Default.GOPATH, + "pkg", "mod", toolkit.GetRelativeDependencyPath("tektoncd/pipeline"), "config", + ), + filepath.Join( + build.Default.GOPATH, + "pkg", "mod", toolkit.GetRelativeDependencyPath("application-api"), + "config", "crd", "bases", + ), + filepath.Join( + build.Default.GOPATH, + "pkg", "mod", toolkit.GetRelativeDependencyPath("release-service"), "config", "crd", "bases", + ), + }, + ErrorIfCRDPathMissing: true, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + Expect(applicationapiv1alpha1.AddToScheme(clientsetscheme.Scheme)).To(Succeed()) + Expect(tektonv1beta1.AddToScheme(clientsetscheme.Scheme)).To(Succeed()) + Expect(releasev1alpha1.AddToScheme(clientsetscheme.Scheme)).To(Succeed()) + Expect(v1beta1.AddToScheme(clientsetscheme.Scheme)).To(Succeed()) + + k8sManager, _ := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: clientsetscheme.Scheme, + MetricsBindAddress: "0", // this disables metrics + LeaderElection: false, + }) + + k8sClient = k8sManager.GetClient() + go func() { + defer GinkgoRecover() + Expect(k8sManager.Start(ctx)).To(Succeed()) + }() +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/controllers/component/predicates.go b/controllers/component/predicates.go new file mode 100644 index 000000000..fbf064c9a --- /dev/null +++ b/controllers/component/predicates.go @@ -0,0 +1,25 @@ +package component + +import ( + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// ComponentDeletedPredicate returns a predicate which filters out +// only deleted component scenarios +func ComponentDeletedPredicate() predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(createEvent event.CreateEvent) bool { + return false + }, + DeleteFunc: func(deleteEvent event.DeleteEvent) bool { + return true + }, + GenericFunc: func(genericEvent event.GenericEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + } +} diff --git a/docs/component_controller.md b/docs/component_controller.md new file mode 100644 index 000000000..f43b02443 --- /dev/null +++ b/docs/component_controller.md @@ -0,0 +1,30 @@ +```mermaid +%%{init: {'theme':'forest'}}%% +flowchart TD + %% Defining the styles + classDef Red fill:#FF9999; + classDef Amber fill:#FFDEAD; + classDef Green fill:#BDFFA4; + + +predicate_deletion_detected((PREDICATE:
Component
is detected as deleted.)) + +%%%%%%%%%%%%%%%%%%%%%%% Drawing EnsureComponentIsCleanedUp() function + +%% Node definitions +isThereReamainingComponent{"Are there other
Components
associated with the
application?"} +stopProcessing[/Controller stops processing.../] +continueProcessing[/Controller continues processing.../] +createUpdatedSnapshot("Create a new snapshot for
remaining components") + +%% Node connections + +predicate_deletion_detected ----> |"EnsureComponentIsCleanedUp()"|isThereReamainingComponent +isThereReamainingComponent --No--> stopProcessing +isThereReamainingComponent --Yes--> createUpdatedSnapshot +createUpdatedSnapshot ----> continueProcessing + + +%% Assigning styles to nodes +class predicate_deletion_detected Amber; +``` diff --git a/gitops/snapshot.go b/gitops/snapshot.go index 739324bd4..b02696b63 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -456,7 +456,7 @@ func PrepareSnapshot(adapterClient client.Client, ctx context.Context, applicati log.Error(nil, "component cannot be added to snapshot for application due to missing containerImage", "component.Name", applicationComponent.Name) continue } - // if the containerImage donesn't have a valid digest, the component + // if the containerImage doesn't have a valid digest, the component // will not be added to snapshot err := ValidateImageDigest(containerImage) if err != nil {