Skip to content

Commit

Permalink
[DEVHAS-380] Have the Application controller add/remove components fr…
Browse files Browse the repository at this point in the history
…om its model (#363)

* Manage Component in App Model

Signed-off-by: John Collier <[email protected]>

* [DEVHAS-380] Have the Application controller add/remove components from its model

Signed-off-by: John Collier <[email protected]>

* [DEVHAS-380] Have the Application controller add/remove components from its model

Signed-off-by: John Collier <[email protected]>

* Cleanup

Signed-off-by: John Collier <[email protected]>

* Fix tests

Signed-off-by: John Collier <[email protected]>

* Cleanup and fix test error

Signed-off-by: John Collier <[email protected]>

* Go fmt

Signed-off-by: John Collier <[email protected]>

* Update controllers/mapper.go

Co-authored-by: Maysun Faisal <[email protected]>

---------

Signed-off-by: John Collier <[email protected]>
Co-authored-by: Maysun Faisal <[email protected]>
  • Loading branch information
johnmcollier and maysunfaisal authored Aug 1, 2023
1 parent f6d10c4 commit 692c54f
Show file tree
Hide file tree
Showing 6 changed files with 629 additions and 132 deletions.
53 changes: 51 additions & 2 deletions controllers/application_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package controllers
import (
"context"
"fmt"
"reflect"
"time"

"github.com/prometheus/client_golang/prometheus"
Expand All @@ -32,12 +33,15 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"

appstudiov1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
Expand Down Expand Up @@ -172,6 +176,15 @@ func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request)
r.SetCreateConditionAndUpdateCR(ctx, req, &application, err)
return reconcile.Result{}, err
}

// Find all components owned by the application
err = r.getAndAddComponentApplicationsToModel(log, req, application.Name, devfileData.GetDevfileWorkspaceSpec())
if err != nil {
r.SetCreateConditionAndUpdateCR(ctx, req, &application, err)
log.Error(err, fmt.Sprintf("Unable to add components to application model for %v", req.NamespacedName))
return ctrl.Result{}, err
}

yamlData, err := yaml.Marshal(devfileData)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to marshall Application devfile, exiting reconcile loop %v", req.NamespacedName))
Expand All @@ -197,11 +210,24 @@ func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

updateRequired := false
// nil out the attributes and projects for the application devfile
// The Attributes contain any image components for the application
// And the projects contains any git components for the application
devWorkspacesSpec := devfileData.GetDevfileWorkspaceSpec().DeepCopy()
devWorkspacesSpec.Attributes = nil
devWorkspacesSpec.Projects = nil

err = r.getAndAddComponentApplicationsToModel(log, req, application.Name, devWorkspacesSpec)
if err != nil {
r.SetUpdateConditionAndUpdateCR(ctx, req, &application, err)
log.Error(err, fmt.Sprintf("Unable to add components to application model for %v", req.NamespacedName))
return ctrl.Result{}, err
}
// Update any specific fields that changed
displayName := application.Spec.DisplayName
description := application.Spec.Description
devfileMeta := devfileData.GetMetadata()
updateRequired := false
if devfileMeta.Name != displayName {
devfileMeta.Name = displayName
updateRequired = true
Expand All @@ -210,10 +236,17 @@ func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request)
devfileMeta.Description = description
updateRequired = true
}

oldDevSpec := devfileData.GetDevfileWorkspaceSpec()
if !reflect.DeepEqual(oldDevSpec.Attributes, devWorkspacesSpec.Attributes) || !reflect.DeepEqual(oldDevSpec.Projects, devWorkspacesSpec.Projects) {
devfileData.SetDevfileWorkspaceSpec(*devWorkspacesSpec)
updateRequired = true
}

if updateRequired {
devfileData.SetMetadata(devfileMeta)

// Update the hasApp CR with the new devfile
// Update the Application CR with the new devfile
yamlData, err := yaml.Marshal(devfileData)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to marshall Application devfile, exiting reconcile loop %v", req.NamespacedName))
Expand All @@ -224,6 +257,7 @@ func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request)
application.Status.Devfile = string(yamlData)
r.SetUpdateConditionAndUpdateCR(ctx, req, &application, nil)
}

}

log.Info(fmt.Sprintf("Finished reconcile loop for %v", req.NamespacedName))
Expand Down Expand Up @@ -257,5 +291,20 @@ func (r *ApplicationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M
return false
},
}).
// Watch Components (Create and Delete events only) as a secondary resource
Watches(&source.Kind{Type: &appstudiov1alpha1.Component{}}, handler.EnqueueRequestsFromMapFunc(MapComponentToApplication()), builder.WithPredicates(predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
return true
},
UpdateFunc: func(e event.UpdateEvent) bool {
return false
},
DeleteFunc: func(e event.DeleteEvent) bool {
return true
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
})).
Complete(r)
}
24 changes: 0 additions & 24 deletions controllers/component_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,6 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err
}

err = r.updateApplicationDevfileModel(hasAppDevfileData, component)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to update the HAS Application Devfile model %v", req.NamespacedName))
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
}

yamlHASCompData, err := yaml.Marshal(compDevfileData)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to marshall the Component devfile, exiting reconcile loop %v", req.NamespacedName))
Expand All @@ -423,23 +416,6 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (

component.Status.Devfile = string(yamlHASCompData)

// Update the HASApp CR with the new devfile
yamlHASAppData, err := yaml.Marshal(hasAppDevfileData)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to marshall the Application devfile, exiting reconcile loop %v", req.NamespacedName))
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
}
hasApplication.Status.Devfile = string(yamlHASAppData)
err = r.Status().Update(ctx, &hasApplication)
if err != nil {
log.Error(err, "Unable to update Application")
// if we're unable to update the Application CR, then we need to err out
// since we need to save a reference of the Component in Application
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
}

// Set the container image in the status
component.Status.ContainerImage = component.Spec.ContainerImage

Expand Down
20 changes: 20 additions & 0 deletions controllers/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,23 @@ func MapToBindingByBoundObjectName(cl client.Client, objectType, label string) f
return req
}
}

// MapComponentToApplication returns an event handler that will convert events on a Component CR to events on its parent Application
func MapComponentToApplication() func(object client.Object) []reconcile.Request {
return func(obj client.Object) []reconcile.Request {
component := obj.(*appstudiov1alpha1.Component)

if component != nil && component.Spec.Application != "" {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: component.Namespace,
Name: component.Spec.Application,
},
},
}
}
// the obj was not in the namespace or it did not have the required Application.
return []reconcile.Request{}
}
}
63 changes: 63 additions & 0 deletions controllers/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,69 @@ func TestMapToBindingByBoundObject(t *testing.T) {
})
}

func TestMapApplicationToComponent(t *testing.T) {

const (
HASAppName = "test-app"
HASCompName = "test-comp"
Namespace = "default"
DisplayName = "an application"
ComponentName = "backend"
SampleRepoLink = "https://github.com/devfile-samples/devfile-sample-java-springboot-basic"
)

applicationName := HASAppName + "1"
componentName := HASCompName + "1"
componentName2 := HASCompName + "2"

componentOne := appstudiov1alpha1.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "appstudio.redhat.com/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName,
Namespace: Namespace,
},
Spec: appstudiov1alpha1.ComponentSpec{
ComponentName: componentName,
Application: applicationName,
},
}
componentTwo := appstudiov1alpha1.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "appstudio.redhat.com/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName2,
Namespace: Namespace,
},
Spec: appstudiov1alpha1.ComponentSpec{
ComponentName: componentName2,
},
}

//fakeClient := NewFakeClient(t, componentOne, applicationOne)

t.Run("should return component's parent application", func(t *testing.T) {
// when
requests := MapComponentToApplication()(&componentOne)

// then
require.Len(t, requests, 1) // binding4 is not returned because binding4 does not have a label matching the staging env
assert.Contains(t, requests, newRequest(applicationName))
})

t.Run("should return no Application requests when Component app name is nil", func(t *testing.T) {
// when
requests := MapComponentToApplication()(&componentTwo)

// then
require.Empty(t, requests)
})
}

func newRequest(name string) reconcile.Request {
return reconcile.Request{
NamespacedName: types.NamespacedName{
Expand Down
120 changes: 73 additions & 47 deletions controllers/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func (r *ComponentReconciler) updateComponentDevfileModel(req ctrl.Request, hasCompDevfileData data.DevfileData, component appstudiov1alpha1.Component) error {
Expand Down Expand Up @@ -216,62 +218,86 @@ func (r *ComponentReconciler) updateComponentDevfileModel(req ctrl.Request, hasC
return nil
}

func (r *ComponentReconciler) updateApplicationDevfileModel(hasAppDevfileData data.DevfileData, component appstudiov1alpha1.Component) error {

if component.Spec.Source.GitSource != nil {
newProject := devfileAPIV1.Project{
Name: component.Spec.ComponentName,
ProjectSource: devfileAPIV1.ProjectSource{
Git: &devfileAPIV1.GitProjectSource{
GitLikeProjectSource: devfileAPIV1.GitLikeProjectSource{
Remotes: map[string]string{
"origin": component.Spec.Source.GitSource.URL,
// addComponentsToApplicationDevfileModel updates the Application's devfile model to include all of the
func (r *ApplicationReconciler) addComponentsToApplicationDevfileModel(devSpec *devfileAPIV1.DevWorkspaceTemplateSpec, components []appstudiov1alpha1.Component) error {

for _, component := range components {
if component.Spec.Source.GitSource != nil {
newProject := devfileAPIV1.Project{
Name: component.Spec.ComponentName,
ProjectSource: devfileAPIV1.ProjectSource{
Git: &devfileAPIV1.GitProjectSource{
GitLikeProjectSource: devfileAPIV1.GitLikeProjectSource{
Remotes: map[string]string{
"origin": component.Spec.Source.GitSource.URL,
},
},
},
},
},
}
projects, err := hasAppDevfileData.GetProjects(common.DevfileOptions{})
if err != nil {
return err
}
for _, project := range projects {
if project.Name == newProject.Name {
return fmt.Errorf("application already has a component with name %s", newProject.Name)
}
}
err = hasAppDevfileData.AddProjects([]devfileAPIV1.Project{newProject})
if err != nil {
return err
}
} else if component.Spec.ContainerImage != "" {
var err error

// Initialize the attributes
devSpec := hasAppDevfileData.GetDevfileWorkspaceSpec()
projects := devSpec.Projects
for _, project := range projects {
if project.Name == newProject.Name {
return fmt.Errorf("application already has a component with name %s", newProject.Name)
}
}
devSpec.Projects = append(devSpec.Projects, newProject)
} else if component.Spec.ContainerImage != "" {
var err error

// Add the image as a top level attribute
devfileAttributes := devSpec.Attributes
if devfileAttributes == nil {
devfileAttributes = attributes.Attributes{}
devSpec.Attributes = devfileAttributes
hasAppDevfileData.SetDevfileWorkspaceSpec(*devSpec)
}
imageAttrString := fmt.Sprintf("containerImage/%s", component.Spec.ComponentName)
componentImage := devfileAttributes.GetString(imageAttrString, &err)
if err != nil {
if _, ok := err.(*attributes.KeyNotFoundError); !ok {
return err
// Add the image as a top level attribute
devfileAttributes := devSpec.Attributes
if devfileAttributes == nil {
devfileAttributes = attributes.Attributes{}
devSpec.Attributes = devfileAttributes
}
imageAttrString := fmt.Sprintf("containerImage/%s", component.Spec.ComponentName)
componentImage := devfileAttributes.GetString(imageAttrString, &err)
if err != nil {
if _, ok := err.(*attributes.KeyNotFoundError); !ok {
return err
}
}
if componentImage != "" {
return fmt.Errorf("application already has a component with name %s", component.Name)
}
devSpec.Attributes = devfileAttributes.PutString(imageAttrString, component.Spec.ContainerImage)

} else {
return fmt.Errorf("component source is nil")
}
if componentImage != "" {
return fmt.Errorf("application already has a component with name %s", component.Name)

}

return nil
}

// getAndAddComponentApplicationsToModel retrieves the list of components that belong to the application CR and adds them to the application's devfile model
func (r *ApplicationReconciler) getAndAddComponentApplicationsToModel(log logr.Logger, req reconcile.Request, applicationName string, devSpec *devfileAPIV1.DevWorkspaceTemplateSpec) error {

// Find all components owned by the application
var components []appstudiov1alpha1.Component
var componentList appstudiov1alpha1.ComponentList
var err error
err = r.Client.List(ctx, &componentList, &client.ListOptions{
Namespace: req.NamespacedName.Namespace,
})
if err != nil {
log.Error(err, fmt.Sprintf("Unable to list Components for %v", req.NamespacedName))
return err
}

for _, component := range componentList.Items {
if component.Spec.Application == applicationName {
components = append(components, component)
}
devSpec.Attributes = devfileAttributes.PutString(imageAttrString, component.Spec.ContainerImage)
hasAppDevfileData.SetDevfileWorkspaceSpec(*devSpec)
}

} else {
return fmt.Errorf("component source is nil")
// Add the components to the Devfile model
err = r.addComponentsToApplicationDevfileModel(devSpec, components)
if err != nil {
log.Error(err, fmt.Sprintf("Error adding components to devfile for Application %v", req.NamespacedName))
return err
}

return nil
Expand Down
Loading

0 comments on commit 692c54f

Please sign in to comment.