From a751e00c0a5f32c953cdab69ffc64f39f99e65c3 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Tue, 28 Feb 2023 10:01:38 -0300 Subject: [PATCH] [KOGITO-8649] - Externalize application properties via ConfigMap (#73) * [KOGITO-8649] - Externalize application properties via ConfigMap Signed-off-by: Ricardo Zanini * Fix erroneous replacements Signed-off-by: Ricardo Zanini * Fix leftovers Signed-off-by: Ricardo Zanini * Using properties package, keep a default var properties Signed-off-by: Ricardo Zanini --------- Signed-off-by: Ricardo Zanini --- controllers/profiles/object_creators.go | 68 +++++++++++++++++- controllers/profiles/object_creators_test.go | 54 ++++++++++++++ controllers/profiles/reconciler_dev.go | 74 ++++++++++++++------ controllers/profiles/reconciler_dev_test.go | 17 ++++- go.mod | 1 + go.sum | 2 + 6 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 controllers/profiles/object_creators_test.go diff --git a/controllers/profiles/object_creators.go b/controllers/profiles/object_creators.go index 99ac31e12..b5d4d5bb2 100644 --- a/controllers/profiles/object_creators.go +++ b/controllers/profiles/object_creators.go @@ -16,7 +16,9 @@ package profiles import ( "context" + "strconv" + "github.com/magiconair/properties" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,8 +37,16 @@ const ( defaultHTTPServicePort = 80 labelApp = "app" kogitoWorkflowJSONFileExt = ".sw.json" + + applicationPropertiesFileName = "application.properties" + + workflowConfigMapNameSuffix = "-props" + configMapWorkflowPropsVolumeName = "workflow-properties" ) +var defaultApplicationProperties = "quarkus.http.port=" + strconv.Itoa(defaultHTTPWorkflowPort) + "\n" + + "quarkus.http.host=0.0.0.0\n" + // objectCreator is the func that creates the initial reference object, if the object doesn't exist in the cluster, this one is created. // Can be used as a reference to keep the object immutable type objectCreator func(workflow *operatorapi.KogitoServerlessWorkflow) (client.Object, error) @@ -168,8 +178,8 @@ func defaultServiceMutateVisitor(workflow *operatorapi.KogitoServerlessWorkflow) } } -// workflowSpecConfigMapCreator creates a new ConfigMap that holds the definition of a workflow specification. -func workflowSpecConfigMapCreator(workflow *operatorapi.KogitoServerlessWorkflow) (client.Object, error) { +// workflowDefConfigMapCreator creates a new ConfigMap that holds the definition of a workflow specification. +func workflowDefConfigMapCreator(workflow *operatorapi.KogitoServerlessWorkflow) (client.Object, error) { workflowDef, err := utils.GetJSONWorkflow(workflow, context.TODO()) if err != nil { return nil, err @@ -192,7 +202,7 @@ func ensureWorkflowSpecConfigMapMutator(workflow *operatorapi.KogitoServerlessWo if kubeutil.IsObjectNew(object) { return nil } - original, err := workflowSpecConfigMapCreator(workflow) + original, err := workflowDefConfigMapCreator(workflow) if err != nil { return err } @@ -202,3 +212,55 @@ func ensureWorkflowSpecConfigMapMutator(workflow *operatorapi.KogitoServerlessWo } } } + +// workflowPropsConfigMapCreator creates a ConfigMap to hold the external application properties +func workflowPropsConfigMapCreator(workflow *operatorapi.KogitoServerlessWorkflow) (client.Object, error) { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: getWorkflowPropertiesConfigMapName(workflow), + Namespace: workflow.Namespace, + Labels: labels(workflow), + }, + // we could use utils.NewJavaProperties, but this way is faster + Data: map[string]string{applicationPropertiesFileName: defaultApplicationProperties}, + }, nil +} + +func getWorkflowPropertiesConfigMapName(workflow *operatorapi.KogitoServerlessWorkflow) string { + return workflow.Name + workflowConfigMapNameSuffix +} + +func ensureWorkflowPropertiesConfigMapMutator(workflow *operatorapi.KogitoServerlessWorkflow) mutateVisitor { + return func(object client.Object) controllerutil.MutateFn { + return func() error { + if kubeutil.IsObjectNew(object) { + return nil + } + original, err := workflowPropsConfigMapCreator(workflow) + if err != nil { + return err + } + cm := object.(*corev1.ConfigMap) + cm.Labels = original.GetLabels() + + _, hasKey := cm.Data[applicationPropertiesFileName] + if !hasKey { + cm.Data = make(map[string]string, 1) + cm.Data[applicationPropertiesFileName] = defaultApplicationProperties + } else { + props, propErr := properties.LoadString(cm.Data[applicationPropertiesFileName]) + if propErr != nil { + // can't load user's properties, replace with default + cm.Data[applicationPropertiesFileName] = defaultApplicationProperties + return nil + } + originalProps := properties.MustLoadString(original.(*corev1.ConfigMap).Data[applicationPropertiesFileName]) + // we overwrite with the defaults + props.Merge(originalProps) + cm.Data[applicationPropertiesFileName] = props.String() + } + + return nil + } + } +} diff --git a/controllers/profiles/object_creators_test.go b/controllers/profiles/object_creators_test.go new file mode 100644 index 000000000..c2c96c1b8 --- /dev/null +++ b/controllers/profiles/object_creators_test.go @@ -0,0 +1,54 @@ +// Copyright 2023 Red Hat, Inc. and/or its affiliates +// +// 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 profiles + +import ( + "testing" + + "github.com/magiconair/properties" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + + "github.com/kiegroup/kogito-serverless-operator/test" +) + +func Test_ensureWorkflowPropertiesConfigMapMutator(t *testing.T) { + workflow := test.GetKogitoServerlessWorkflow("../../config/samples/"+test.KogitoServerlessWorkflowSampleDevModeYamlCR, t.Name()) + // can't be new + cm, _ := workflowPropsConfigMapCreator(workflow) + cm.SetUID("1") + cm.SetResourceVersion("1") + reflectCm := cm.(*v1.ConfigMap) + + visitor := ensureWorkflowPropertiesConfigMapMutator(workflow) + mutateFn := visitor(cm) + + assert.NoError(t, mutateFn()) + assert.NotEmpty(t, reflectCm.Data[applicationPropertiesFileName]) + + props := properties.MustLoadString(reflectCm.Data[applicationPropertiesFileName]) + 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[applicationPropertiesFileName] = "quarkus.http.port=9090\nmy.new.prop=1" + visitor(reflectCm) + assert.NoError(t, mutateFn()) + + // we should preserve the default, and still got ours + props = properties.MustLoadString(reflectCm.Data[applicationPropertiesFileName]) + 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", "")) +} diff --git a/controllers/profiles/reconciler_dev.go b/controllers/profiles/reconciler_dev.go index 74c786636..a6e5954fc 100644 --- a/controllers/profiles/reconciler_dev.go +++ b/controllers/profiles/reconciler_dev.go @@ -34,13 +34,17 @@ import ( var _ ProfileReconciler = &developmentProfile{} const ( - // TODO: read from the platform config, open a JIRA to track it down. Default tag MUST align with the current operator's version. See: https://issues.redhat.com/browse/KOGITO-8675 + // TODO: read from the platform config. Default tag MUST align with the current operator's version. See: https://issues.redhat.com/browse/KOGITO-8675 defaultKogitoServerlessWorkflowDevImage = "quay.io/kiegroup/kogito-swf-builder-nightly:latest" - configMapWorkflowDefVolumeNamePrefix = "wd" + configMapWorkflowDefVolumeName = "workflow-definition" configMapWorkflowDefMountPath = "/home/kogito/serverless-workflow-project/src/main/resources/workflows" - requeueAfterFailure = 3 * time.Minute - requeueAfterFollowDeployment = 10 * time.Second - requeueAfterIsRunning = 3 * time.Minute + // quarkusDevConfigMountPath mount path for application properties file in the Workflow Quarkus Application + // + // See: https://quarkus.io/guides/config-reference#application-properties-file + quarkusDevConfigMountPath = "/home/kogito/serverless-workflow-project/src/main/resources" + requeueAfterFailure = 3 * time.Minute + requeueAfterFollowDeployment = 10 * time.Second + requeueAfterIsRunning = 3 * time.Minute // recoverDeploymentErrorRetries how many times the operator should try to recover from a failure before giving up recoverDeploymentErrorRetries = 3 // recoverDeploymentErrorInterval interval between recovering from failures @@ -76,16 +80,18 @@ func newDevProfileReconciler(client client.Client, logger *logr.Logger) ProfileR func newDevelopmentObjectEnsurers(support *stateSupport) *devProfileObjectEnsurers { return &devProfileObjectEnsurers{ - deployment: newObjectEnsurer(support.client, support.logger, defaultDeploymentCreator), - service: newObjectEnsurer(support.client, support.logger, defaultServiceCreator), - workflowConfigMap: newObjectEnsurer(support.client, support.logger, workflowSpecConfigMapCreator), + deployment: newObjectEnsurer(support.client, support.logger, defaultDeploymentCreator), + service: newObjectEnsurer(support.client, support.logger, defaultServiceCreator), + definitionConfigMap: newObjectEnsurer(support.client, support.logger, workflowDefConfigMapCreator), + propertiesConfigMap: newObjectEnsurer(support.client, support.logger, workflowPropsConfigMapCreator), } } type devProfileObjectEnsurers struct { - deployment *objectEnsurer - service *objectEnsurer - workflowConfigMap *objectEnsurer + deployment *objectEnsurer + service *objectEnsurer + definitionConfigMap *objectEnsurer + propertiesConfigMap *objectEnsurer } type ensureRunningDevWorkflowReconciliationState struct { @@ -101,16 +107,22 @@ func (e *ensureRunningDevWorkflowReconciliationState) CanReconcile(workflow *ope func (e *ensureRunningDevWorkflowReconciliationState) Do(ctx context.Context, workflow *operatorapi.KogitoServerlessWorkflow) (ctrl.Result, []client.Object, error) { var objs []client.Object - configMap, _, err := e.ensurers.workflowConfigMap.ensure(ctx, workflow, ensureWorkflowSpecConfigMapMutator(workflow)) + flowDefCM, _, err := e.ensurers.definitionConfigMap.ensure(ctx, workflow, ensureWorkflowSpecConfigMapMutator(workflow)) if err != nil { return ctrl.Result{Requeue: false}, objs, err } - objs = append(objs, configMap) + objs = append(objs, flowDefCM) + + propsCM, _, err := e.ensurers.propertiesConfigMap.ensure(ctx, workflow, ensureWorkflowPropertiesConfigMapMutator(workflow)) + if err != nil { + return ctrl.Result{Requeue: false}, objs, err + } + objs = append(objs, propsCM) deployment, _, err := e.ensurers.deployment.ensure(ctx, workflow, defaultDeploymentMutateVisitor(workflow), naiveApplyImageDeploymentMutateVisitor(defaultKogitoServerlessWorkflowDevImage), - mountWorkflowDefConfigMapMutateVisitor(configMap.(*v1.ConfigMap))) + mountDevConfigMapsMutateVisitor(flowDefCM.(*v1.ConfigMap), propsCM.(*v1.ConfigMap))) if err != nil { return ctrl.Result{RequeueAfter: requeueAfterFailure}, objs, err } @@ -254,28 +266,44 @@ func getDeploymentFailureReasonOrDefaultReason(deployment *appsv1.Deployment) st return failure } -// mountWorkflowDefConfigMapMutateVisitor mounts the given ConfigMap workflows definitions into the dev container -func mountWorkflowDefConfigMapMutateVisitor(cm *v1.ConfigMap) mutateVisitor { +// mountDevConfigMapsMutateVisitor mounts the required configMaps in the Workflow Dev Deployment +func mountDevConfigMapsMutateVisitor(flowDefCM, propsCM *v1.ConfigMap) mutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) volumes := make([]v1.Volume, 0) volumeMounts := make([]v1.VolumeMount, 0) - volumes = append(volumes, v1.Volume{ - Name: configMapWorkflowDefVolumeNamePrefix, - VolumeSource: v1.VolumeSource{ - ConfigMap: &v1.ConfigMapVolumeSource{ - LocalObjectReference: v1.LocalObjectReference{Name: cm.Name}, + volumes = append(volumes, + v1.Volume{ + Name: configMapWorkflowDefVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: flowDefCM.Name}, + }, }, }, - }) + v1.Volume{ + Name: configMapWorkflowPropsVolumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: propsCM.Name}, + Items: []v1.KeyToPath{{Key: applicationPropertiesFileName, Path: applicationPropertiesFileName}}, + }, + }, + }) + volumeMounts = append(volumeMounts, v1.VolumeMount{ - Name: configMapWorkflowDefVolumeNamePrefix, + Name: configMapWorkflowDefVolumeName, ReadOnly: true, MountPath: configMapWorkflowDefMountPath, }, + v1.VolumeMount{ + Name: configMapWorkflowPropsVolumeName, + ReadOnly: true, + MountPath: quarkusDevConfigMountPath, + }, ) deployment.Spec.Template.Spec.Volumes = make([]v1.Volume, 0) diff --git a/controllers/profiles/reconciler_dev_test.go b/controllers/profiles/reconciler_dev_test.go index 09b57e05e..6f2c4e8f7 100644 --- a/controllers/profiles/reconciler_dev_test.go +++ b/controllers/profiles/reconciler_dev_test.go @@ -19,6 +19,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" clientruntime "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -85,10 +87,16 @@ func Test_newDevProfile(t *testing.T) { deployment := test.MustGetDeployment(t, client, workflow) assert.Equal(t, defaultKogitoServerlessWorkflowDevImage, deployment.Spec.Template.Spec.Containers[0].Image) - cm := test.MustGetConfigMap(t, client, workflow) - assert.NotEmpty(t, cm.Data[workflow.Name+kogitoWorkflowJSONFileExt]) + defCM := test.MustGetConfigMap(t, client, workflow) + assert.NotEmpty(t, defCM.Data[workflow.Name+kogitoWorkflowJSONFileExt]) assert.Equal(t, configMapWorkflowDefMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) + propCM := &v1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: getWorkflowPropertiesConfigMapName(workflow)}, propCM) + assert.NotEmpty(t, propCM.Data[applicationPropertiesFileName]) + assert.Equal(t, quarkusDevConfigMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[1].MountPath) + assert.Contains(t, propCM.Data[applicationPropertiesFileName], "quarkus.http.port") + service := test.MustGetService(t, client, workflow) assert.Equal(t, int32(defaultHTTPWorkflowPort), service.Spec.Ports[0].TargetPort.IntVal) @@ -116,6 +124,11 @@ func Test_newDevProfile(t *testing.T) { err = client.Update(context.TODO(), deployment) assert.NoError(t, err) + propCM = &v1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: getWorkflowPropertiesConfigMapName(workflow)}, propCM) + assert.NotEmpty(t, propCM.Data[applicationPropertiesFileName]) + assert.Contains(t, propCM.Data[applicationPropertiesFileName], "quarkus.http.port") + // reconcile workflow.Status.Condition = operatorapi.RunningConditionType err = client.Update(context.TODO(), workflow) diff --git a/go.mod b/go.mod index 0bbeb549a..93a92cba2 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/moby/spdystream v0.2.0 // indirect diff --git a/go.sum b/go.sum index 796795ff7..9412469d9 100644 --- a/go.sum +++ b/go.sum @@ -275,6 +275,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=