diff --git a/controllers/profiles/common/ensurer.go b/controllers/profiles/common/ensurer.go index 0d9cb472e..7d258dbb3 100644 --- a/controllers/profiles/common/ensurer.go +++ b/controllers/profiles/common/ensurer.go @@ -36,6 +36,9 @@ var _ ObjectEnsurer = &noopObjectEnsurer{} type ObjectEnsurer interface { Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) } +type ObjectEnsurerWithPlatform interface { + Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) +} // MutateVisitor is a visitor function that mutates the given object before performing any updates in the cluster. // It gets called after the objectEnforcer reference. @@ -56,6 +59,14 @@ func NewObjectEnsurer(client client.Client, creator ObjectCreator) ObjectEnsurer } } +// NewObjectEnsurerWithPlatform see defaultObjectEnsurerWithPLatform +func NewObjectEnsurerWithPlatform(client client.Client, creator ObjectCreatorWithPlatform) ObjectEnsurerWithPlatform { + return &defaultObjectEnsurerWithPlatform{ + c: client, + creator: creator, + } +} + // defaultObjectEnsurer provides the engine for a ReconciliationState that needs to create or update a given Kubernetes object during the reconciliation cycle. type defaultObjectEnsurer struct { c client.Client @@ -84,6 +95,34 @@ func (d *defaultObjectEnsurer) Ensure(ctx context.Context, workflow *operatorapi return object, result, nil } +// defaultObjectEnsurerWithPlatform is the equivalent of defaultObjectEnsurer for resources that require a reference to the SonataFlowPlatform +type defaultObjectEnsurerWithPlatform struct { + c client.Client + creator ObjectCreatorWithPlatform +} + +func (d *defaultObjectEnsurerWithPlatform) Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, pl *operatorapi.SonataFlowPlatform, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) { + result := controllerutil.OperationResultNone + + object, err := d.creator(workflow, pl) + if err != nil { + return nil, result, err + } + if result, err = controllerutil.CreateOrPatch(ctx, d.c, object, + func() error { + for _, v := range visitors { + if visitorErr := v(object)(); visitorErr != nil { + return visitorErr + } + } + return controllerutil.SetControllerReference(workflow, object, d.c.Scheme()) + }); err != nil { + return nil, result, err + } + klog.V(log.I).InfoS("Object operation finalized", "result", result, "kind", object.GetObjectKind().GroupVersionKind().String(), "name", object.GetName(), "namespace", object.GetNamespace()) + return object, result, nil +} + // NewNoopObjectEnsurer see noopObjectEnsurer func NewNoopObjectEnsurer() ObjectEnsurer { return &noopObjectEnsurer{} diff --git a/controllers/profiles/common/mutate_visitors.go b/controllers/profiles/common/mutate_visitors.go index ffe9784a3..8ccbcab35 100644 --- a/controllers/profiles/common/mutate_visitors.go +++ b/controllers/profiles/common/mutate_visitors.go @@ -105,32 +105,28 @@ func ServiceMutateVisitor(workflow *operatorapi.SonataFlow) MutateVisitor { } } -func WorkflowPropertiesMutateVisitor(ctx context.Context, catalog discovery.ServiceCatalog, - workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) MutateVisitor { +func ManagedPropertiesMutateVisitor(ctx context.Context, catalog discovery.ServiceCatalog, + workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform, userProps *corev1.ConfigMap) MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { - if kubeutil.IsObjectNew(object) { - return nil - } - cm := object.(*corev1.ConfigMap) - cm.Labels = workflow.GetLabels() - _, hasKey := cm.Data[workflowproj.ApplicationPropertiesFileName] + managedProps := object.(*corev1.ConfigMap) + managedProps.Labels = workflow.GetLabels() + _, hasKey := managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] if !hasKey { - cm.Data = make(map[string]string, 1) - props, err := properties.ImmutableApplicationProperties(workflow, platform) - if err != nil { - return err - } - cm.Data[workflowproj.ApplicationPropertiesFileName] = props - return nil + managedProps.Data = make(map[string]string, 1) + managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] = "" } + userProperties, hasKey := userProps.Data[workflowproj.ApplicationPropertiesFileName] + if !hasKey { + userProperties = "" + } // In the future, if this needs change, instead we can receive an AppPropertyHandler in this mutator props, err := properties.NewAppPropertyHandler(workflow, platform) if err != nil { return err } - cm.Data[workflowproj.ApplicationPropertiesFileName] = props.WithUserProperties(cm.Data[workflowproj.ApplicationPropertiesFileName]). + managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] = props.WithUserProperties(userProperties). WithServiceDiscovery(ctx, catalog). Build() return nil @@ -142,11 +138,11 @@ func WorkflowPropertiesMutateVisitor(ctx context.Context, catalog discovery.Serv // This method can be used as an alternative to the Kubernetes ConfigMap refresher. // // See: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically -func RolloutDeploymentIfCMChangedMutateVisitor(cm *v1.ConfigMap) MutateVisitor { +func RolloutDeploymentIfCMChangedMutateVisitor(workflow *operatorapi.SonataFlow, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) - err := kubeutil.AnnotateDeploymentConfigChecksum(deployment, cm) + err := kubeutil.AnnotateDeploymentConfigChecksum(workflow, deployment, userPropsCM, managedPropsCM) return err } } diff --git a/controllers/profiles/common/object_creators.go b/controllers/profiles/common/object_creators.go index 5739b6ad5..0e2d6f209 100644 --- a/controllers/profiles/common/object_creators.go +++ b/controllers/profiles/common/object_creators.go @@ -41,6 +41,10 @@ import ( // Can be used as a reference to keep the object immutable type ObjectCreator func(workflow *operatorapi.SonataFlow) (client.Object, error) +// ObjectCreatorWithPlatform is the func equivalent to ObjectCreator to use when the resource being created needs a reference to the +// SonataFlowPlatform +type ObjectCreatorWithPlatform func(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (client.Object, error) + const ( defaultHTTPServicePort = 80 @@ -209,13 +213,18 @@ func OpenShiftRouteCreator(workflow *operatorapi.SonataFlow) (client.Object, err return route, err } -// WorkflowPropsConfigMapCreator creates a ConfigMap to hold the external application properties -func WorkflowPropsConfigMapCreator(workflow *operatorapi.SonataFlow) (client.Object, error) { - props, err := properties.ImmutableApplicationProperties(workflow, nil) +// UserPropsConfigMapCreator creates an empty ConfigMap to hold the user application properties +func UserPropsConfigMapCreator(workflow *operatorapi.SonataFlow) (client.Object, error) { + return workflowproj.CreateNewUserPropsConfigMap(workflow), nil +} + +// ManagedPropsConfigMapCreator creates an empty ConfigMap to hold the external application properties +func ManagedPropsConfigMapCreator(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (client.Object, error) { + props, err := properties.ImmutableApplicationProperties(workflow, platform) if err != nil { return nil, err } - return workflowproj.CreateNewAppPropsConfigMap(workflow, props), nil + return workflowproj.CreateNewManagedPropsConfigMap(workflow, props), nil } func ConfigurePersistence(serviceContainer *corev1.Container, options *operatorapi.PersistenceOptions, defaultSchema, namespace string) *corev1.Container { diff --git a/controllers/profiles/common/object_creators_test.go b/controllers/profiles/common/object_creators_test.go index 64e8f43b7..ae252248e 100644 --- a/controllers/profiles/common/object_creators_test.go +++ b/controllers/profiles/common/object_creators_test.go @@ -27,7 +27,6 @@ import ( "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/apache/incubator-kie-kogito-serverless-operator/utils" kubeutil "github.com/apache/incubator-kie-kogito-serverless-operator/utils/kubernetes" @@ -39,50 +38,56 @@ import ( func Test_ensureWorkflowPropertiesConfigMapMutator(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) + platform := test.GetBasePlatform() // can't be new - cm, _ := WorkflowPropsConfigMapCreator(workflow) - cm.SetUID("1") - cm.SetResourceVersion("1") - reflectCm := cm.(*corev1.ConfigMap) + managedProps, _ := ManagedPropsConfigMapCreator(workflow, platform) + managedProps.SetUID("1") + managedProps.SetResourceVersion("1") + managedPropsCM := managedProps.(*corev1.ConfigMap) - visitor := WorkflowPropertiesMutateVisitor(context.TODO(), nil, workflow, nil) - mutateFn := visitor(cm) + userProps, _ := UserPropsConfigMapCreator(workflow) + userPropsCM := userProps.(*corev1.ConfigMap) + visitor := ManagedPropertiesMutateVisitor(context.TODO(), nil, workflow, nil, userPropsCM) + mutateFn := visitor(managedProps) assert.NoError(t, mutateFn()) - assert.NotEmpty(t, reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + assert.Empty(t, managedPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) - props := properties.MustLoadString(reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + props := properties.MustLoadString(managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, "8080", props.GetString("quarkus.http.port", "")) // we change the properties to something different, we add ours and change the default - reflectCm.Data[workflowproj.ApplicationPropertiesFileName] = "quarkus.http.port=9090\nmy.new.prop=1" - visitor(reflectCm) + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = "quarkus.http.port=9090\nmy.new.prop=1" + visitor(managedPropsCM) assert.NoError(t, mutateFn()) // we should preserve the default, and still got ours - props = properties.MustLoadString(reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + props = properties.MustLoadString(managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, "8080", props.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", props.GetString("quarkus.http.host", "")) - assert.Equal(t, "1", props.GetString("my.new.prop", "")) + assert.NotContains(t, "my.new.prop", props.Keys()) } func Test_ensureWorkflowPropertiesConfigMapMutator_DollarReplacement(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) - existingCM := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: workflow.Name, - Namespace: workflow.Namespace, - UID: "0000-0001-0002-0003", - }, - Data: map[string]string{ - workflowproj.ApplicationPropertiesFileName: "mp.messaging.outgoing.kogito_outgoing_stream.url=${kubernetes:services.v1/event-listener}", - }, - } - mutateVisitorFn := WorkflowPropertiesMutateVisitor(context.TODO(), nil, workflow, nil) + platform := test.GetBasePlatform() + managedProps, _ := ManagedPropsConfigMapCreator(workflow, platform) + managedProps.SetName(workflow.Name) + managedProps.SetNamespace(workflow.Namespace) + managedProps.SetUID("0000-0001-0002-0003") + managedPropsCM := managedProps.(*corev1.ConfigMap) + + userProps, _ := UserPropsConfigMapCreator(workflow) + userPropsCM := userProps.(*corev1.ConfigMap) + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = "mp.messaging.outgoing.kogito_outgoing_stream.url=${kubernetes:services.v1/event-listener}" + + mutateVisitorFn := ManagedPropertiesMutateVisitor(context.TODO(), nil, workflow, nil, userPropsCM) - err := mutateVisitorFn(existingCM)() + err := mutateVisitorFn(managedPropsCM)() assert.NoError(t, err) - assert.Contains(t, existingCM.Data[workflowproj.ApplicationPropertiesFileName], "${kubernetes:services.v1/event-listener}") + assert.NotContains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "mp.messaging.outgoing.kogito_outgoing_stream.url") + // assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "${kubernetes:services.v1/event-listener}") } func TestMergePodSpec(t *testing.T) { diff --git a/controllers/profiles/common/properties/application.go b/controllers/profiles/common/properties/application.go index fda4ec1e4..414891405 100644 --- a/controllers/profiles/common/properties/application.go +++ b/controllers/profiles/common/properties/application.go @@ -75,36 +75,34 @@ func (a *appPropertyHandler) WithServiceDiscovery(ctx context.Context, catalog d } func (a *appPropertyHandler) Build() string { - var props *properties.Properties + var userProps *properties.Properties var propErr error = nil if len(a.userProperties) == 0 { - props = properties.NewProperties() + userProps = properties.NewProperties() } else { - props, propErr = properties.LoadString(a.userProperties) + userProps, propErr = properties.LoadString(a.userProperties) } if propErr != nil { klog.V(log.D).InfoS("Can't load user's property", "workflow", a.workflow.Name, "namespace", a.workflow.Namespace, "properties", a.userProperties) - props = properties.NewProperties() + userProps = properties.NewProperties() } // Disable expansions since it's not our responsibility // Property expansion means resolving ${} within the properties and environment context. Quarkus will do that in runtime. - props.DisableExpansion = true + userProps.DisableExpansion = true - removeDiscoveryProperties(props) + removeDiscoveryProperties(userProps) + discoveryProps := properties.NewProperties() if a.requireServiceDiscovery() { // produce the MicroProfileConfigServiceCatalog properties for the service discovery property values if any. - discoveryProperties := generateDiscoveryProperties(a.ctx, a.catalog, props, a.workflow) - if discoveryProperties.Len() > 0 { - props.Merge(discoveryProperties) - } + discoveryProps.Merge(generateDiscoveryProperties(a.ctx, a.catalog, userProps, a.workflow)) } - props = utils.NewApplicationPropertiesBuilder(). - WithInitialProperties(props). + userProps = utils.NewApplicationPropertiesBuilder(). + WithInitialProperties(discoveryProps). WithImmutableProperties(properties.MustLoadString(immutableApplicationProperties)). WithDefaultMutableProperties(a.defaultMutableProperties). Build() - return props.String() + return userProps.String() } // withKogitoServiceUrl adds the property kogitoServiceUrlProperty to the application properties. @@ -135,13 +133,14 @@ func (a *appPropertyHandler) addDefaultMutableProperty(name string, value string } // NewAppPropertyHandler creates a property handler for a given workflow to execute in the provided platform. -// This handler is intended to build the application properties required by the workflow to execute properly, note that -// the produced properties might vary depending on the platfom, for example, if the job service managed by the platform +// This handler is intended to build the managed application properties required by the workflow to execute properly together with +// the user properties defined in the user-managed ConfigMap. +// Note that the produced properties might vary depending on the platfom, for example, if the job service managed by the platform // a particular set of properties will be added, etc. // By default, the following properties are incorporated: // The set of immutable properties provided by the operator. (user can never change) -// The set of defaultMutableProperties that are provided by the operator, and that the user might overwrite if it changes -// the workflow ConfigMap. This set includes for example the required properties to connect with the data index and the +// The set of defaultMutableProperties that are provided by the operator, and that the user cannot overwrite even if it changes +// the user-managed ConfigMap. This set includes for example the required properties to connect with the data index and the // job service when any of these services are managed by the platform. func NewAppPropertyHandler(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (AppPropertyHandler, error) { handler := &appPropertyHandler{ diff --git a/controllers/profiles/common/properties/application_test.go b/controllers/profiles/common/properties/application_test.go index 543247dd7..d0f4d5dff 100644 --- a/controllers/profiles/common/properties/application_test.go +++ b/controllers/profiles/common/properties/application_test.go @@ -124,9 +124,9 @@ func Test_appPropertyHandler_WithUserPropertiesWithNoUserOverrides(t *testing.T) assert.NoError(t, err) generatedProps, propsErr := properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 9, len(generatedProps.Keys())) - assert.Equal(t, "value1", generatedProps.GetString("property1", "")) - assert.Equal(t, "value2", generatedProps.GetString("property2", "")) + assert.Equal(t, 7, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "http://greeting.default", generatedProps.GetString("kogito.service.url", "")) assert.Equal(t, "8080", generatedProps.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", generatedProps.GetString("quarkus.http.host", "")) @@ -157,13 +157,16 @@ func Test_appPropertyHandler_WithUserPropertiesWithServiceDiscovery(t *testing.T Build()) generatedProps.DisableExpansion = true assert.NoError(t, propsErr) - assert.Equal(t, 23, len(generatedProps.Keys())) - assertHasProperty(t, generatedProps, "property1", "value1") - assertHasProperty(t, generatedProps, "property2", "value2") - - assertHasProperty(t, generatedProps, "service1", "${kubernetes:services.v1/namespace1/my-service1}") - assertHasProperty(t, generatedProps, "service2", "${kubernetes:services.v1/my-service2}") - assertHasProperty(t, generatedProps, "service3", "${knative:namespace1/my-kn-service1}") + assert.Equal(t, 21, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) + assertHasProperty(t, generatedProps, "service1", myService1Address) + assertHasProperty(t, generatedProps, "service2", myService2Address) + assertHasProperty(t, generatedProps, "service3", myKnService1Address) + assertHasProperty(t, generatedProps, "service4", myKnService2Address) + assertHasProperty(t, generatedProps, "service5", myKnService3Address) + assertHasProperty(t, generatedProps, "broker1", myKnBroker1Address) + assertHasProperty(t, generatedProps, "broker2", myKnBroker2Address) //org.kie.kogito.addons.discovery.kubernetes\:services.v1\/usecase1ยบ/my-service1 below we use the unescaped vale because the properties.LoadString removes them. assertHasProperty(t, generatedProps, "org.kie.kogito.addons.discovery.kubernetes:services.v1/namespace1/my-service1", myService1Address) @@ -214,12 +217,12 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr := properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 13, len(generatedProps.Keys())) - assert.Equal(t, "value1", generatedProps.GetString("property1", "")) - assert.Equal(t, "value2", generatedProps.GetString("property2", "")) + assert.Equal(t, 11, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) - //kogito.service.url takes the user provided value since it's a default mutable property. - assert.Equal(t, "http://myUrl.override.com", generatedProps.GetString("kogito.service.url", "")) + //kogito.service.url is a default immutable property. + assert.Equal(t, "http://greeting.default", generatedProps.GetString("kogito.service.url", "")) //quarkus.http.port remains with the default value since it's immutable. assert.Equal(t, "8080", generatedProps.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", generatedProps.GetString("quarkus.http.host", "")) @@ -239,7 +242,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 18, len(generatedProps.Keys())) + assert.Equal(t, 16, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/definitions", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "true", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/processes", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -259,7 +264,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 14, len(generatedProps.Keys())) + assert.Equal(t, 12, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "false", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -276,7 +283,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 13, len(generatedProps.Keys())) + assert.Equal(t, 11, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "false", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) diff --git a/controllers/profiles/common/properties/discovery.go b/controllers/profiles/common/properties/discovery.go index a7663d419..6d2ac71bb 100644 --- a/controllers/profiles/common/properties/discovery.go +++ b/controllers/profiles/common/properties/discovery.go @@ -99,6 +99,8 @@ func generateDiscoveryProperties(ctx context.Context, catalog discovery.ServiceC mpProperty := generateMicroprofileServiceCatalogProperty(plainUri) klog.V(log.I).Infof("Generating microprofile service catalog property %s=%s.", mpProperty, address) result.MustSet(mpProperty, address) + klog.V(log.I).Infof("Overriding the discoverable value as the managed property %s=%s.", k, address) + result.MustSet(k, address) } } } diff --git a/controllers/profiles/common/properties/discovery_test.go b/controllers/profiles/common/properties/discovery_test.go index 4555c6c38..dd502d184 100644 --- a/controllers/profiles/common/properties/discovery_test.go +++ b/controllers/profiles/common/properties/discovery_test.go @@ -62,7 +62,12 @@ func Test_generateDiscoveryProperties(t *testing.T) { Spec: v1alpha08.SonataFlowSpec{Flow: workflow}, }) - assert.Equal(t, result.Len(), 5) + assert.Equal(t, 8, result.Len()) + assertHasProperty(t, result, "service1", myService1Address) + assertHasProperty(t, result, "service2", myService2Address) + assertHasProperty(t, result, "service3", myService3Address) + assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) + assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/my-service2", myService2Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/my-service3?port\\=http-port", myService3Address) diff --git a/controllers/profiles/dev/object_creators_dev.go b/controllers/profiles/dev/object_creators_dev.go index 1cbb42e4c..03b857502 100644 --- a/controllers/profiles/dev/object_creators_dev.go +++ b/controllers/profiles/dev/object_creators_dev.go @@ -118,7 +118,7 @@ func ensureWorkflowDefConfigMapMutator(workflow *operatorapi.SonataFlow) common. } // mountDevConfigMapsMutateVisitor mounts the required configMaps in the Workflow Dev Deployment -func mountDevConfigMapsMutateVisitor(flowDefCM, propsCM *corev1.ConfigMap, workflowResCMs []operatorapi.ConfigMapWorkflowResource) common.MutateVisitor { +func mountDevConfigMapsMutateVisitor(workflow *operatorapi.SonataFlow, flowDefCM, userPropsCM, managedPropsCM *corev1.ConfigMap, workflowResCMs []operatorapi.ConfigMapWorkflowResource) common.MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) @@ -129,7 +129,8 @@ func mountDevConfigMapsMutateVisitor(flowDefCM, propsCM *corev1.ConfigMap, workf // defaultResourcesVolume holds every ConfigMap mount required on src/main/resources defaultResourcesVolume := corev1.Volume{Name: configMapResourcesVolumeName, VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{}}} - kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, propsCM.Name, corev1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, userPropsCM.Name, corev1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, managedPropsCM.Name, corev1.KeyToPath{Key: workflowproj.GetManagedPropertiesFileName(workflow), Path: workflowproj.GetManagedPropertiesFileName(workflow)}) kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, flowDefCM.Name) // resourceVolumes holds every resource that needs to be mounted on src/main/resources/ diff --git a/controllers/profiles/dev/object_creators_dev_test.go b/controllers/profiles/dev/object_creators_dev_test.go index 37adaf8a4..8209940a2 100644 --- a/controllers/profiles/dev/object_creators_dev_test.go +++ b/controllers/profiles/dev/object_creators_dev_test.go @@ -30,7 +30,6 @@ import ( func Test_ensureWorkflowDevServiceIsExposed(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) - //On Kubernetes we want the service exposed in Dev with NodePort service, _ := serviceCreator(workflow) service.SetUID("1") diff --git a/controllers/profiles/dev/profile_dev.go b/controllers/profiles/dev/profile_dev.go index dd0ae728d..42b9151ab 100644 --- a/controllers/profiles/dev/profile_dev.go +++ b/controllers/profiles/dev/profile_dev.go @@ -75,21 +75,23 @@ func NewProfileReconciler(client client.Client, cfg *rest.Config, recorder recor func newObjectEnsurers(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, deploymentCreator), - service: common.NewObjectEnsurer(support.C, serviceCreator), - network: common.NewNoopObjectEnsurer(), - definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, deploymentCreator), + service: common.NewObjectEnsurer(support.C, serviceCreator), + network: common.NewNoopObjectEnsurer(), + definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } func newObjectEnsurersOpenShift(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, deploymentCreator), - service: common.NewObjectEnsurer(support.C, serviceCreator), - network: common.NewObjectEnsurer(support.C, common.OpenShiftRouteCreator), - definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, deploymentCreator), + service: common.NewObjectEnsurer(support.C, serviceCreator), + network: common.NewObjectEnsurer(support.C, common.OpenShiftRouteCreator), + definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } @@ -106,11 +108,12 @@ func newStatusEnrichersOpenShift(support *common.StateSupport) *statusEnrichers } type objectEnsurers struct { - deployment common.ObjectEnsurer - service common.ObjectEnsurer - network common.ObjectEnsurer - definitionConfigMap common.ObjectEnsurer - propertiesConfigMap common.ObjectEnsurer + deployment common.ObjectEnsurer + service common.ObjectEnsurer + network common.ObjectEnsurer + definitionConfigMap common.ObjectEnsurer + userPropsConfigMap common.ObjectEnsurer + managedPropsConfigMap common.ObjectEnsurerWithPlatform } type statusEnrichers struct { diff --git a/controllers/profiles/dev/profile_dev_test.go b/controllers/profiles/dev/profile_dev_test.go index 5c2591d0b..1c3478ea0 100644 --- a/controllers/profiles/dev/profile_dev_test.go +++ b/controllers/profiles/dev/profile_dev_test.go @@ -145,12 +145,16 @@ func Test_newDevProfile(t *testing.T) { assert.Equal(t, quarkusDevConfigMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) assert.Equal(t, "", deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath) //https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically - propCM := &corev1.ConfigMap{} - _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowPropertiesConfigMapName(workflow)}, propCM) - assert.NotEmpty(t, propCM.Data[workflowproj.ApplicationPropertiesFileName]) + userPropsCM := &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow)}, userPropsCM) + assert.Empty(t, userPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + + managedPropsCM := &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow)}, managedPropsCM) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, quarkusDevConfigMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) assert.Equal(t, "", deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath) //https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically - assert.Contains(t, propCM.Data[workflowproj.ApplicationPropertiesFileName], "quarkus.http.port") + assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "quarkus.http.port") service := test.MustGetService(t, client, workflow) assert.Equal(t, int32(constants.DefaultHTTPWorkflowPortInt), service.Spec.Ports[0].TargetPort.IntVal) @@ -179,10 +183,14 @@ func Test_newDevProfile(t *testing.T) { err = client.Update(context.TODO(), deployment) assert.NoError(t, err) - propCM = &corev1.ConfigMap{} - _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowPropertiesConfigMapName(workflow)}, propCM) - assert.NotEmpty(t, propCM.Data[workflowproj.ApplicationPropertiesFileName]) - assert.Contains(t, propCM.Data[workflowproj.ApplicationPropertiesFileName], "quarkus.http.port") + userPropsCM = &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow)}, userPropsCM) + assert.Empty(t, userPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + + managedPropsCM = &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow)}, managedPropsCM) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) + assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "quarkus.http.port") // reconcile workflow.Status.Manager().MarkTrue(api.RunningConditionType) diff --git a/controllers/profiles/dev/states_dev.go b/controllers/profiles/dev/states_dev.go index e965547ef..f98e03eb2 100644 --- a/controllers/profiles/dev/states_dev.go +++ b/controllers/profiles/dev/states_dev.go @@ -74,11 +74,15 @@ func (e *ensureRunningWorkflowState) Do(ctx context.Context, workflow *operatora if err == nil && len(pl.Spec.DevMode.BaseImage) > 0 { devBaseContainerImage = pl.Spec.DevMode.BaseImage } - propsCM, _, err := e.ensurers.propertiesConfigMap.Ensure(ctx, workflow, common.WorkflowPropertiesMutateVisitor(ctx, e.StateSupport.Catalog, workflow, pl)) + userPropsCM, _, err := e.ensurers.userPropsConfigMap.Ensure(ctx, workflow) if err != nil { return ctrl.Result{Requeue: false}, objs, err } - objs = append(objs, propsCM) + managedPropsCM, _, err := e.ensurers.managedPropsConfigMap.Ensure(ctx, workflow, pl, common.ManagedPropertiesMutateVisitor(ctx, e.StateSupport.Catalog, workflow, pl, userPropsCM.(*corev1.ConfigMap))) + if err != nil { + return ctrl.Result{Requeue: false}, objs, err + } + objs = append(objs, managedPropsCM) externalCM, err := workflowdef.FetchExternalResourcesConfigMapsRef(e.C, workflow) if err != nil { @@ -92,7 +96,7 @@ func (e *ensureRunningWorkflowState) Do(ctx context.Context, workflow *operatora deployment, _, err := e.ensurers.deployment.Ensure(ctx, workflow, deploymentMutateVisitor(workflow), common.ImageDeploymentMutateVisitor(workflow, devBaseContainerImage), - mountDevConfigMapsMutateVisitor(flowDefCM.(*corev1.ConfigMap), propsCM.(*corev1.ConfigMap), externalCM)) + mountDevConfigMapsMutateVisitor(workflow, flowDefCM.(*corev1.ConfigMap), userPropsCM.(*corev1.ConfigMap), managedPropsCM.(*corev1.ConfigMap), externalCM)) if err != nil { return ctrl.Result{RequeueAfter: constants.RequeueAfterFailure}, objs, err } diff --git a/controllers/profiles/prod/deployment_handler.go b/controllers/profiles/prod/deployment_handler.go index a5459a873..3b693429d 100644 --- a/controllers/profiles/prod/deployment_handler.go +++ b/controllers/profiles/prod/deployment_handler.go @@ -49,9 +49,16 @@ func (d *deploymentReconciler) reconcile(ctx context.Context, workflow *operator func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, workflow *operatorapi.SonataFlow, image string) (reconcile.Result, []client.Object, error) { pl, _ := platform.GetActivePlatform(ctx, d.C, workflow.Namespace) - propsCM, _, err := d.ensurers.propertiesConfigMap.Ensure(ctx, workflow, common.WorkflowPropertiesMutateVisitor(ctx, d.StateSupport.Catalog, workflow, pl)) + userPropsCM, _, err := d.ensurers.userPropsConfigMap.Ensure(ctx, workflow) if err != nil { - workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the properties config map") + workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the user properties config map") + _, err = d.PerformStatusUpdate(ctx, workflow) + return ctrl.Result{}, nil, err + } + managedPropsCM, _, err := d.ensurers.managedPropsConfigMap.Ensure(ctx, workflow, pl, + common.ManagedPropertiesMutateVisitor(ctx, d.StateSupport.Catalog, workflow, pl, userPropsCM.(*v1.ConfigMap))) + if err != nil { + workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the managed properties config map") _, err = d.PerformStatusUpdate(ctx, workflow) return ctrl.Result{}, nil, err } @@ -60,7 +67,7 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work d.ensurers.deployment.Ensure( ctx, workflow, - d.getDeploymentMutateVisitors(workflow, image, propsCM.(*v1.ConfigMap))..., + d.getDeploymentMutateVisitors(workflow, image, userPropsCM.(*v1.ConfigMap), managedPropsCM.(*v1.ConfigMap))..., ) if err != nil { workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.DeploymentUnavailableReason, "Unable to perform the deploy due to ", err) @@ -75,7 +82,7 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work return reconcile.Result{}, nil, err } - objs := []client.Object{deployment, service, propsCM} + objs := []client.Object{deployment, service, managedPropsCM} if deploymentOp == controllerutil.OperationResultCreated { workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.WaitingForDeploymentReason, "") @@ -100,17 +107,18 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work func (d *deploymentReconciler) getDeploymentMutateVisitors( workflow *operatorapi.SonataFlow, image string, - configMap *v1.ConfigMap) []common.MutateVisitor { + userPropsCM *v1.ConfigMap, + managedPropsCM *v1.ConfigMap) []common.MutateVisitor { if utils.IsOpenShift() { return []common.MutateVisitor{common.DeploymentMutateVisitor(workflow), - mountProdConfigMapsMutateVisitor(configMap), + mountProdConfigMapsMutateVisitor(workflow, userPropsCM, managedPropsCM), addOpenShiftImageTriggerDeploymentMutateVisitor(workflow, image), common.ImageDeploymentMutateVisitor(workflow, image), - common.RolloutDeploymentIfCMChangedMutateVisitor(configMap), + common.RolloutDeploymentIfCMChangedMutateVisitor(workflow, userPropsCM, managedPropsCM), } } return []common.MutateVisitor{common.DeploymentMutateVisitor(workflow), common.ImageDeploymentMutateVisitor(workflow, image), - mountProdConfigMapsMutateVisitor(configMap), - common.RolloutDeploymentIfCMChangedMutateVisitor(configMap)} + mountProdConfigMapsMutateVisitor(workflow, userPropsCM, managedPropsCM), + common.RolloutDeploymentIfCMChangedMutateVisitor(workflow, userPropsCM, managedPropsCM)} } diff --git a/controllers/profiles/prod/deployment_handler_test.go b/controllers/profiles/prod/deployment_handler_test.go index 5adfae686..a133b9635 100644 --- a/controllers/profiles/prod/deployment_handler_test.go +++ b/controllers/profiles/prod/deployment_handler_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) @@ -77,8 +78,12 @@ func Test_CheckDeploymentRolloutAfterCMChange(t *testing.T) { assert.NotEmpty(t, objects) assert.True(t, result.Requeue) + userPropsCM := &corev1.ConfigMap{} + err = client.Get(context.TODO(), types.NamespacedName{Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow), Namespace: t.Name()}, userPropsCM) + assert.NoError(t, err) + // Second reconciliation, we do change the configmap and that must rollout the deployment - var cm *corev1.ConfigMap + var managedPropsCM *corev1.ConfigMap var checksum string for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { @@ -89,16 +94,20 @@ func Test_CheckDeploymentRolloutAfterCMChange(t *testing.T) { assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) } if _, ok := o.(*corev1.ConfigMap); ok { - cm = o.(*corev1.ConfigMap) - currentProps := cm.Data[workflowproj.ApplicationPropertiesFileName] - props, err := properties.LoadString(currentProps) - assert.Nil(t, err) - props.MustSet("test.property", "test.value") - cm.Data[workflowproj.ApplicationPropertiesFileName] = props.String() + cm := o.(*corev1.ConfigMap) + if cm.Name == workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow) { + managedPropsCM = cm + } } } - assert.NotNil(t, cm) - utilruntime.Must(client.Update(context.TODO(), cm)) + assert.NotNil(t, managedPropsCM) + + currentProps := userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] + props, err := properties.LoadString(currentProps) + assert.Nil(t, err) + props.MustSet("test.property", "test.value") + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = props.String() + utilruntime.Must(client.Update(context.TODO(), userPropsCM)) result, objects, err = handler.reconcile(context.TODO(), workflow) assert.NoError(t, err) assert.NotEmpty(t, objects) @@ -131,9 +140,13 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { assert.NotEmpty(t, objects) assert.True(t, result.Requeue) + userPropsCM := &corev1.ConfigMap{} + err = client.Get(context.TODO(), types.NamespacedName{Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow), Namespace: t.Name()}, userPropsCM) + assert.NoError(t, err) + // Second reconciliation, we do change the configmap and that must not rollout the deployment // because we're not updating the application.properties key - var cm *corev1.ConfigMap + var managedPropsCM *corev1.ConfigMap var checksum string for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { @@ -144,12 +157,16 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) } if _, ok := o.(*corev1.ConfigMap); ok { - cm = o.(*corev1.ConfigMap) - cm.Data["other.key"] = "useless.key = value" + cm := o.(*corev1.ConfigMap) + if cm.Name == workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow) { + managedPropsCM = cm + } } } - assert.NotNil(t, cm) - utilruntime.Must(client.Update(context.TODO(), cm)) + assert.NotNil(t, managedPropsCM) + + userPropsCM.Data["other.key"] = "useless.key = value" + utilruntime.Must(client.Update(context.TODO(), userPropsCM)) result, objects, err = handler.reconcile(context.TODO(), workflow) assert.NoError(t, err) assert.NotEmpty(t, objects) @@ -157,13 +174,10 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { deployment := o.(*v1.Deployment) - // Commented while waiting for SRVLOGIC-195 to be addressed - // assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) - assert.Contains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.Checksum) + assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) newChecksum := deployment.Spec.Template.ObjectMeta.Annotations[metadata.Checksum] assert.NotEmpty(t, newChecksum) - // Change to asssert.Equal when SRVLOGIC-195 is addressed - assert.NotEqual(t, newChecksum, checksum) + assert.Equal(t, newChecksum, checksum) break } } diff --git a/controllers/profiles/prod/object_creators_prod.go b/controllers/profiles/prod/object_creators_prod.go index 34d1447ec..02328e9b4 100644 --- a/controllers/profiles/prod/object_creators_prod.go +++ b/controllers/profiles/prod/object_creators_prod.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common" "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/constants" @@ -64,7 +65,7 @@ func addOpenShiftImageTriggerDeploymentMutateVisitor(workflow *v1alpha08.SonataF } // mountDevConfigMapsMutateVisitor mounts the required configMaps in the Workflow Dev Deployment -func mountProdConfigMapsMutateVisitor(propsCM *v1.ConfigMap) common.MutateVisitor { +func mountProdConfigMapsMutateVisitor(workflow *operatorapi.SonataFlow, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) common.MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) @@ -77,12 +78,14 @@ func mountProdConfigMapsMutateVisitor(propsCM *v1.ConfigMap) common.MutateVisito deployment.Spec.Template.Spec.Containers[idx].VolumeMounts = make([]v1.VolumeMount, 0, 1) } - kubeutil.AddOrReplaceVolume(&deployment.Spec.Template.Spec, - kubeutil.VolumeConfigMap(constants.ConfigMapWorkflowPropsVolumeName, propsCM.Name, v1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName})) + defaultResourcesVolume := v1.Volume{Name: constants.ConfigMapWorkflowPropsVolumeName, VolumeSource: v1.VolumeSource{Projected: &v1.ProjectedVolumeSource{}}} + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, userPropsCM.Name, v1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, managedPropsCM.Name, v1.KeyToPath{Key: workflowproj.GetManagedPropertiesFileName(workflow), Path: workflowproj.GetManagedPropertiesFileName(workflow)}) + kubeutil.AddOrReplaceVolume(&deployment.Spec.Template.Spec, defaultResourcesVolume) kubeutil.AddOrReplaceVolumeMount(idx, &deployment.Spec.Template.Spec, kubeutil.VolumeMount(constants.ConfigMapWorkflowPropsVolumeName, true, quarkusProdConfigMountPath)) - kubeutil.AnnotateDeploymentConfigChecksum(deployment, propsCM) + kubeutil.AnnotateDeploymentConfigChecksum(workflow, deployment, userPropsCM, managedPropsCM) return nil } } diff --git a/controllers/profiles/prod/profile_prod.go b/controllers/profiles/prod/profile_prod.go index 062359ee1..f5046d05f 100644 --- a/controllers/profiles/prod/profile_prod.go +++ b/controllers/profiles/prod/profile_prod.go @@ -52,16 +52,18 @@ const ( // ReconciliationState that needs access to it must include this struct as an attribute and initialize it in the profile builder. // Use newObjectEnsurers to facilitate building this struct type objectEnsurers struct { - deployment common.ObjectEnsurer - service common.ObjectEnsurer - propertiesConfigMap common.ObjectEnsurer + deployment common.ObjectEnsurer + service common.ObjectEnsurer + userPropsConfigMap common.ObjectEnsurer + managedPropsConfigMap common.ObjectEnsurerWithPlatform } func newObjectEnsurers(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, common.DeploymentCreator), - service: common.NewObjectEnsurer(support.C, common.ServiceCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, common.DeploymentCreator), + service: common.NewObjectEnsurer(support.C, common.ServiceCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } diff --git a/utils/kubernetes/deployment.go b/utils/kubernetes/deployment.go index ab8dd9409..676c49924 100644 --- a/utils/kubernetes/deployment.go +++ b/utils/kubernetes/deployment.go @@ -27,6 +27,7 @@ import ( "time" "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" "github.com/apache/incubator-kie-kogito-serverless-operator/log" "github.com/apache/incubator-kie-kogito-serverless-operator/workflowproj" appsv1 "k8s.io/api/apps/v1" @@ -112,7 +113,7 @@ func MarkDeploymentToRollout(deployment *appsv1.Deployment) error { // AnnotateDeploymentConfigChecksum adds the checksum/config annotation to the template annotations of the Deployment to set the current configuration. // If the checksum has changed from the previous value, the restartedAt annotation is also added and a new rollout is started. // Code adapted from here: https://github.com/kubernetes/kubectl/blob/release-1.26/pkg/polymorphichelpers/objectrestarter.go#L44 -func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.ConfigMap) error { +func AnnotateDeploymentConfigChecksum(workflow *operatorapi.SonataFlow, deployment *appsv1.Deployment, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) error { if deployment.Spec.Paused { return errors.New("can't restart paused deployment (run rollout resume first)") } @@ -124,7 +125,7 @@ func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.Conf if !ok { currentChecksum = "" } - newChecksum, err := configMapChecksum(cm) + newChecksum, err := calculateHash(userPropsCM, managedPropsCM, workflow) if err != nil { return err } @@ -141,14 +142,19 @@ func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.Conf return nil } -func configMapChecksum(cm *v1.ConfigMap) (string, error) { - props, hasKey := cm.Data[workflowproj.ApplicationPropertiesFileName] +func dataFromCM(cm *v1.ConfigMap, key string) string { + data, hasKey := cm.Data[key] if !hasKey { - props = "" + return "" } + return data +} +func calculateHash(userPropsCM, managedPropsCM *v1.ConfigMap, workflow *operatorapi.SonataFlow) (string, error) { + aggregatedProps := fmt.Sprintf("%s,%s", dataFromCM(userPropsCM, workflowproj.ApplicationPropertiesFileName), + dataFromCM(managedPropsCM, workflowproj.GetManagedPropertiesFileName(workflow))) hash := sha256.New() - _, err := hash.Write([]byte(props)) + _, err := hash.Write([]byte(aggregatedProps)) if err != nil { return "", err } diff --git a/workflowproj/operator.go b/workflowproj/operator.go index 7821c36ae..33b4ca672 100644 --- a/workflowproj/operator.go +++ b/workflowproj/operator.go @@ -20,18 +20,22 @@ package workflowproj import ( + "fmt" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles" ) const ( - workflowConfigMapNameSuffix = "-props" - // ApplicationPropertiesFileName is the default application properties file name - ApplicationPropertiesFileName = "application.properties" + workflowUserConfigMapNameSuffix = "-props" + // ApplicationPropertiesFileName is the default application properties file name holding user properties + ApplicationPropertiesFileName = "application.properties" + workflowManagedConfigMapNameSuffix = "-managed-props" // LabelApp key to use among object selectors, "app" is used among k8s applications to group objects in some UI consoles LabelApp = "app" // LabelService key to use among object selectors @@ -60,9 +64,23 @@ func SetTypeToObject(obj runtime.Object, s *runtime.Scheme) error { return nil } -// GetWorkflowPropertiesConfigMapName gets the default ConfigMap name that holds the application property for the given workflow -func GetWorkflowPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { - return workflow.Name + workflowConfigMapNameSuffix +// GetWorkflowUserPropertiesConfigMapName gets the default ConfigMap name that holds the user application property for the given workflow +func GetWorkflowUserPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { + return workflow.Name + workflowUserConfigMapNameSuffix +} + +// GetWorkflowManagedPropertiesConfigMapName gets the default ConfigMap name that holds the managed application property for the given workflow +func GetWorkflowManagedPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { + return workflow.Name + workflowManagedConfigMapNameSuffix +} + +// GetWorkflowManagedPropertiesConfigMapName gets the default ConfigMap name that holds the managed application property for the given workflow +func GetManagedPropertiesFileName(workflow *operatorapi.SonataFlow) string { + profile := metadata.ProdProfile + if profiles.IsDevProfile(workflow) { + profile = metadata.DevProfile + } + return fmt.Sprintf("application-%s.properties", profile) } // SetDefaultLabels adds the default workflow application labels to the given object. @@ -80,15 +98,27 @@ func GetDefaultLabels(workflow *operatorapi.SonataFlow) map[string]string { } } -// CreateNewAppPropsConfigMap creates a new ConfigMap object to hold the workflow application properties. -func CreateNewAppPropsConfigMap(workflow *operatorapi.SonataFlow, properties string) *corev1.ConfigMap { +// CreateNewUserPropsConfigMap creates a new empty ConfigMap object to hold the user application properties of the workflow. +func CreateNewUserPropsConfigMap(workflow *operatorapi.SonataFlow) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetWorkflowUserPropertiesConfigMapName(workflow), + Namespace: workflow.Namespace, + Labels: GetDefaultLabels(workflow), + }, + Data: map[string]string{ApplicationPropertiesFileName: ""}, + } +} + +// CreateNewManagedPropsConfigMap creates a new ConfigMap object to hold the managed application properties of the workflos. +func CreateNewManagedPropsConfigMap(workflow *operatorapi.SonataFlow, properties string) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: GetWorkflowPropertiesConfigMapName(workflow), + Name: GetWorkflowManagedPropertiesConfigMapName(workflow), Namespace: workflow.Namespace, Labels: GetDefaultLabels(workflow), }, - Data: map[string]string{ApplicationPropertiesFileName: properties}, + Data: map[string]string{GetManagedPropertiesFileName(workflow): properties}, } } diff --git a/workflowproj/workflowproj.go b/workflowproj/workflowproj.go index 3b46f8c95..afabd7e5c 100644 --- a/workflowproj/workflowproj.go +++ b/workflowproj/workflowproj.go @@ -247,7 +247,8 @@ func (w *workflowProjectHandler) parseRawAppProperties() error { if err != nil { return err } - w.project.Properties = CreateNewAppPropsConfigMap(w.project.Workflow, string(appPropsContent)) + w.project.Properties = CreateNewUserPropsConfigMap(w.project.Workflow) + w.project.Properties.Data[ApplicationPropertiesFileName] = string(appPropsContent) if err = SetTypeToObject(w.project.Properties, w.scheme); err != nil { return err } diff --git a/workflowproj/workflowproj_test.go b/workflowproj/workflowproj_test.go index 0bc9679cf..c1207527f 100644 --- a/workflowproj/workflowproj_test.go +++ b/workflowproj/workflowproj_test.go @@ -61,6 +61,7 @@ func Test_Handler_WorkflowMinimalAndProps(t *testing.T) { assert.NotNil(t, proj.Properties) assert.Equal(t, "minimal", proj.Workflow.Name) assert.Equal(t, "minimal-props", proj.Properties.Name) + assert.NotEmpty(t, proj.Properties.Data["application.properties"]) assert.Equal(t, string(metadata.ProdProfile), proj.Workflow.Annotations[metadata.Profile]) assert.NotEmpty(t, proj.Properties.Data) }