Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/09-Configuration reference/01-Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ While we have some basic types (string, number, bool ...), we also have some com
| payments.worker.temporal-max-concurrent-activity-task-pollers | Int | | Payments worker max concurrent activity task pollers configuration |
| payments.worker.temporal-max-slots-per-poller | Int | | Payments worker max slots per poller |
| payments.worker.temporal-max-local-activity-slots | Int | | Payments worker max local activity slots |
| deployments.`<deployment-name>`.spec.template.annotations | Map | firstannotations=X, anotherannotation=X | |
| deployments.`<deployment-name>`.init-containers.`<container-name>`.resource-requirements | Map | cpu=X, mem=X | |
| deployments.`<deployment-name>`.containers.`<container-name>`.resource-requirements | Map | cpu=X, mem=X | |
| deployments.`<deployment-name>`.init-containers.`<container-name>`.run-as | Map | user=X, group=X | |
| deployments.`<deployment-name>`.containers.`<container-name>`.run-as | Map | user=X, group=X | |
| deployments.`<deployment-name>`.replicas | string | 2 | |
| caddy.image | string | | Caddy image |
| jobs.`<owner-kind>`.spec.template.annotations | Map | firstannotations=X, anotherannotations=Y | Configure the annotations on specific jobs'modules |
| jobs.`<owner-kind>`.init-containers.`<container-name>`.run-as | Map | user=X, group=X | Configure the security context for init containers in jobs by specifying the user and group IDs to run as |
| jobs.`<owner-kind>`.containers.`<container-name>`.run-as | Map | user=X, group=X | Configure the security context for containers in jobs by specifying the user and group IDs to run as |
| registries.`<name>`.endpoint | string | | Specify a custom endpoint for a specific docker repository |
Expand Down
4 changes: 2 additions & 2 deletions helm/operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: "2.11.0"
version: "2.12.0"
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v2.11.0"
appVersion: "v2.12.0"
dependencies:
- name: operator-crds
version: "2.X"
Expand Down
300 changes: 172 additions & 128 deletions internal/resources/applications/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package applications
import (
"encoding/json"
"fmt"
"maps"
"strconv"

"github.com/formancehq/operator/internal/resources/licence"
Expand Down Expand Up @@ -156,43 +157,55 @@ func (a Application) Install(ctx core.Context) error {
return a.handlePDB(ctx, deploymentLabels)
}

func (a Application) handleDeployment(ctx core.Context, deploymentLabels map[string]string) error {
condition := v1beta1.Condition{
Type: "DeploymentReady",
ObservedGeneration: a.owner.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: strcase.UpperCamelCase(a.deploymentTpl.Name),
func (a Application) WithAnnotations(annotations map[string]string) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = map[string]string{}
}

maps.Copy(deployment.Spec.Template.Annotations, annotations)
return nil
}
defer func() {
a.owner.GetConditions().AppendOrReplace(condition, v1beta1.AndConditions(
v1beta1.ConditionTypeMatch("DeploymentReady"),
v1beta1.ConditionReasonMatch(strcase.UpperCamelCase(a.deploymentTpl.Name)),
))
}()
}

gracePeriod, err := settings.GetStringOrDefault(
ctx,
a.owner.GetStack(),
"",
"modules",
strcase.LowerCamelCase(a.owner.GetObjectKind().GroupVersionKind().Kind),
"grace-period",
)
if err != nil {
return fmt.Errorf("failed to get grace period: %w", err)
func (a Application) withSettingAnnotations(ctx core.Context) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
annotations, err := settings.GetMapOrEmpty(ctx, a.owner.GetStack(), "deployments", a.deploymentTpl.Name, "spec", "template", "annotations")
if err != nil {
return err
}

if len(annotations) == 0 {
return nil
}

return a.WithAnnotations(annotations)(deployment)
}
}

func (a Application) containersMutator(ctx core.Context, labels map[string]string) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
gracePeriod, err := settings.GetStringOrDefault(
ctx,
a.owner.GetStack(),
"",
"modules",
strcase.LowerCamelCase(a.owner.GetObjectKind().GroupVersionKind().Kind),
"grace-period",
)
if err != nil {
return fmt.Errorf("failed to get grace period: %w", err)
}

mutators := make([]core.ObjectMutator[*appsv1.Deployment], 0)
mutators = append(mutators, func(deployment *appsv1.Deployment) error {
a.deploymentTpl.Spec.DeepCopyInto(&deployment.Spec)
deployment.SetName(a.deploymentTpl.Name)
deployment.SetNamespace(a.owner.GetStack())

// Configure matching labels
deployment.Spec.Selector = &metav1.LabelSelector{
MatchLabels: deploymentLabels,
MatchLabels: labels,
}
deployment.Spec.Template.Labels = deploymentLabels
deployment.Spec.Template.Labels = labels

Comment on lines 205 to 209
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Do not overwrite pre‑existing labels – merge instead

Assigning deployment.Spec.Template.Labels = labels discards any labels already present in the template copied from a.deploymentTpl (e.g. module, version, stack labels).
Safer approach: make sure the map exists and merge:

-        deployment.Spec.Template.Labels = labels
+        if deployment.Spec.Template.Labels == nil {
+            deployment.Spec.Template.Labels = map[string]string{}
+        }
+        maps.Copy(deployment.Spec.Template.Labels, labels)

Same for deployment.Spec.Selector.MatchLabels if additional selector labels may exist.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deployment.Spec.Selector = &metav1.LabelSelector{
MatchLabels: deploymentLabels,
MatchLabels: labels,
}
deployment.Spec.Template.Labels = deploymentLabels
deployment.Spec.Template.Labels = labels
deployment.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labels,
}
if deployment.Spec.Template.Labels == nil {
deployment.Spec.Template.Labels = map[string]string{}
}
maps.Copy(deployment.Spec.Template.Labels, labels)

// Configure security context
deployment.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{
Expand Down Expand Up @@ -243,81 +256,112 @@ func (a Application) handleDeployment(ctx core.Context, deploymentLabels map[str
}

return nil
})

if !a.stateful {
mutators = append(mutators,
func(t *appsv1.Deployment) error {
replicas, err := settings.GetInt32(ctx, a.owner.GetStack(), "deployments", a.deploymentTpl.Name, "replicas")
if err != nil {
return err
}
t.Spec.Replicas = replicas
}
}

func (a Application) withStatefulHandling(ctx core.Context) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
if !a.stateful {
replicas, err := settings.GetInt32(ctx, a.owner.GetStack(), "deployments", a.deploymentTpl.Name, "replicas")
if err != nil {
return err
}
deployment.Spec.Replicas = replicas

} else {
deployment.Spec.Strategy = appsv1.DeploymentStrategy{
Type: appsv1.RecreateDeploymentStrategyType,
}

}
return nil
}
}

func (a Application) withEELicence(ctx core.Context) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
if a.isEE {
licenceSecretResourceRef, licenceEnv, err := licence.GetLicenceEnvVars(ctx, a.deploymentTpl.Name, a.owner)
if err != nil {
return err
}
if len(licenceEnv) == 0 {
return nil
})
} else {
mutators = append(mutators,
func(t *appsv1.Deployment) error {
t.Spec.Strategy = appsv1.DeploymentStrategy{
Type: appsv1.RecreateDeploymentStrategyType,
}
for i, container := range deployment.Spec.Template.Spec.InitContainers {
container.Env = append(container.Env, licenceEnv...)
deployment.Spec.Template.Spec.InitContainers[i] = container
}
for i, container := range deployment.Spec.Template.Spec.Containers {
container.Env = append(container.Env, licenceEnv...)
deployment.Spec.Template.Spec.Containers[i] = container
}
if licenceSecretResourceRef != nil {
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = map[string]string{}
}
return nil
})
deployment.Spec.Template.Annotations["licence-secret-hash"] = licenceSecretResourceRef.Status.Hash
}

return nil

}
return nil
}
}

if a.isEE {
licenceSecretResourceRef, licenceEnv, err := licence.GetLicenceEnvVars(ctx, a.deploymentTpl.Name, a.owner)
func (a Application) withJsonLogging(ctx core.Context) core.ObjectMutator[*appsv1.Deployment] {
return func(deployment *appsv1.Deployment) error {
isJsonLogging, err := settings.GetBoolOrFalse(ctx, a.owner.GetStack(), "logging", "json")
if err != nil {
return err
}
if len(licenceEnv) > 0 {
mutators = append(mutators, func(t *appsv1.Deployment) error {
for i, container := range t.Spec.Template.Spec.InitContainers {
container.Env = append(container.Env, licenceEnv...)
t.Spec.Template.Spec.InitContainers[i] = container
}
for i, container := range t.Spec.Template.Spec.Containers {
container.Env = append(container.Env, licenceEnv...)
t.Spec.Template.Spec.Containers[i] = container
}
if licenceSecretResourceRef != nil {
if t.Spec.Template.Annotations == nil {
t.Spec.Template.Annotations = map[string]string{}
}
t.Spec.Template.Annotations["licence-secret-hash"] = licenceSecretResourceRef.Status.Hash
}

return nil
})
if !isJsonLogging {
return nil
}
}

mutators = append(mutators, core.WithController[*appsv1.Deployment](ctx.GetScheme(), a.owner))
v := corev1.EnvVar{
Name: "JSON_FORMATTING_LOGGER",
Value: "true",
}
for i, container := range deployment.Spec.Template.Spec.InitContainers {
container.Env = append(container.Env, v)
deployment.Spec.Template.Spec.InitContainers[i] = container
}
for i, container := range deployment.Spec.Template.Spec.Containers {
container.Env = append(container.Env, v)
deployment.Spec.Template.Spec.Containers[i] = container
}

isJsonLogging, err := settings.GetBoolOrFalse(ctx, a.owner.GetStack(), "logging", "json")
if err != nil {
return err
return nil
}
if isJsonLogging {
mutators = append(mutators, func(t *appsv1.Deployment) error {
v := corev1.EnvVar{
Name: "JSON_FORMATTING_LOGGER",
Value: "true",
}
for i, container := range t.Spec.Template.Spec.InitContainers {
container.Env = append(container.Env, v)
t.Spec.Template.Spec.InitContainers[i] = container
}
for i, container := range t.Spec.Template.Spec.Containers {
container.Env = append(container.Env, v)
t.Spec.Template.Spec.Containers[i] = container
}
}

return nil
})
func (a Application) handleDeployment(ctx core.Context, deploymentLabels map[string]string) error {
condition := v1beta1.Condition{
Type: "DeploymentReady",
ObservedGeneration: a.owner.GetGeneration(),
LastTransitionTime: metav1.Now(),
Reason: strcase.UpperCamelCase(a.deploymentTpl.Name),
}
defer func() {
a.owner.GetConditions().AppendOrReplace(condition, v1beta1.AndConditions(
v1beta1.ConditionTypeMatch("DeploymentReady"),
v1beta1.ConditionReasonMatch(strcase.UpperCamelCase(a.deploymentTpl.Name)),
))
}()

deployment, _, err := core.CreateOrUpdate[*appsv1.Deployment](ctx, types.NamespacedName{
mutators := make([]core.ObjectMutator[*appsv1.Deployment], 0)
mutators = append(mutators,
a.containersMutator(ctx, deploymentLabels),
a.withSettingAnnotations(ctx),
a.withStatefulHandling(ctx),
a.withEELicence(ctx),
a.withJsonLogging(ctx),
core.WithController[*appsv1.Deployment](ctx.GetScheme(), a.owner),
)

deployment, _, err := core.CreateOrUpdate(ctx, types.NamespacedName{
Namespace: a.owner.GetStack(),
Name: a.deploymentTpl.Name,
}, mutators...)
Expand Down Expand Up @@ -352,57 +396,57 @@ func (a Application) handlePDB(ctx core.Context, deploymentLabels map[string]str
v1beta1.ConditionReasonMatch(strcase.UpperCamelCase(a.deploymentTpl.Name)),
))
}()
if !a.stateful {

pdb, err := settings.GetAs[podDisruptionBudgetConfiguration](ctx, a.owner.GetStack(), "deployments", a.deploymentTpl.Name, "pod-disruption-budget")
if err != nil {
if a.stateful {
if err := a.deletePDBIfExists(ctx); err != nil {
return err
}

if pdb.MinAvailable != "" || pdb.MaxUnavailable != "" {
podDisruptionBudgetConfiguredCondition := v1beta1.NewCondition("PodDisruptionBudgetConfigured", a.owner.GetGeneration()).
SetReason(strcase.UpperCamelCase(a.deploymentTpl.Name))

defer func() {
a.owner.GetConditions().AppendOrReplace(*podDisruptionBudgetConfiguredCondition, v1beta1.AndConditions(
v1beta1.ConditionTypeMatch("PodDisruptionBudgetConfigured"),
v1beta1.ConditionReasonMatch(strcase.UpperCamelCase(a.deploymentTpl.Name)),
))
}()

_, _, err = core.CreateOrUpdate(ctx, types.NamespacedName{
Namespace: a.owner.GetStack(),
Name: a.deploymentTpl.Name,
}, func(t *v1.PodDisruptionBudget) error {
if pdb.MinAvailable != "" {
t.Spec.MinAvailable = pointer.For(intstr.Parse(pdb.MinAvailable))
}
if pdb.MaxUnavailable != "" {
t.Spec.MaxUnavailable = pointer.For(intstr.Parse(pdb.MaxUnavailable))
}
t.Spec.Selector = &metav1.LabelSelector{
MatchLabels: deploymentLabels,
}
return nil
},
core.WithController[*v1.PodDisruptionBudget](ctx.GetScheme(), a.owner),
)
if err != nil {
podDisruptionBudgetConfiguredCondition.SetStatus(metav1.ConditionFalse).SetMessage(err.Error())
return err
podDisruptionBudgetCondition.SetMessage("application defined as stateful")
return nil
}

pdb, err := settings.GetAs[podDisruptionBudgetConfiguration](ctx, a.owner.GetStack(), "deployments", a.deploymentTpl.Name, "pod-disruption-budget")
if err != nil {
return err
}

if pdb.MinAvailable != "" || pdb.MaxUnavailable != "" {
podDisruptionBudgetConfiguredCondition := v1beta1.NewCondition("PodDisruptionBudgetConfigured", a.owner.GetGeneration()).
SetReason(strcase.UpperCamelCase(a.deploymentTpl.Name))

defer func() {
a.owner.GetConditions().AppendOrReplace(*podDisruptionBudgetConfiguredCondition, v1beta1.AndConditions(
v1beta1.ConditionTypeMatch("PodDisruptionBudgetConfigured"),
v1beta1.ConditionReasonMatch(strcase.UpperCamelCase(a.deploymentTpl.Name)),
))
}()

_, _, err = core.CreateOrUpdate(ctx, types.NamespacedName{
Namespace: a.owner.GetStack(),
Name: a.deploymentTpl.Name,
}, func(t *v1.PodDisruptionBudget) error {
if pdb.MinAvailable != "" {
t.Spec.MinAvailable = pointer.For(intstr.Parse(pdb.MinAvailable))
}
} else {
if err := a.deletePDBIfExists(ctx); err != nil {
return err
if pdb.MaxUnavailable != "" {
t.Spec.MaxUnavailable = pointer.For(intstr.Parse(pdb.MaxUnavailable))
}
t.Spec.Selector = &metav1.LabelSelector{
MatchLabels: deploymentLabels,
}
podDisruptionBudgetCondition.SetMessage("no PDB found")
return nil
},
core.WithController[*v1.PodDisruptionBudget](ctx.GetScheme(), a.owner),
)
if err != nil {
podDisruptionBudgetConfiguredCondition.SetStatus(metav1.ConditionFalse).SetMessage(err.Error())
return err
}
} else {
if err := a.deletePDBIfExists(ctx); err != nil {
return err
}

podDisruptionBudgetCondition.SetMessage("application defined as stateful")
podDisruptionBudgetCondition.SetMessage("no PDB found")
}

return nil
Expand Down
Loading