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 {