Skip to content

Commit

Permalink
[KOGITO-8649] - Externalize application properties via ConfigMap (#73)
Browse files Browse the repository at this point in the history
* [KOGITO-8649] - Externalize application properties via ConfigMap

Signed-off-by: Ricardo Zanini <[email protected]>

* Fix erroneous replacements

Signed-off-by: Ricardo Zanini <[email protected]>

* Fix leftovers

Signed-off-by: Ricardo Zanini <[email protected]>

* Using properties package, keep a default var properties

Signed-off-by: Ricardo Zanini <[email protected]>

---------

Signed-off-by: Ricardo Zanini <[email protected]>
  • Loading branch information
ricardozanini authored Feb 28, 2023
1 parent d28801b commit a751e00
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 28 deletions.
68 changes: 65 additions & 3 deletions controllers/profiles/object_creators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
}
}
54 changes: 54 additions & 0 deletions controllers/profiles/object_creators_test.go
Original file line number Diff line number Diff line change
@@ -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", ""))
}
74 changes: 51 additions & 23 deletions controllers/profiles/reconciler_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions controllers/profiles/reconciler_dev_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit a751e00

Please sign in to comment.