Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 1, 2023
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
Comment on lines +218 to +219
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait why do we do this again

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make comparisons of the added git projects (under projects in the devfile spec) and container image projects (under attributes) easier in the devfile app model. Basically we create a copy of the devworkspace spec, nil out the attributes and projects, add the components to it, and compare with the existing devworkspace spec.


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