mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
439 lines
14 KiB
Go
439 lines
14 KiB
Go
package ledgers
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/formancehq/operator/internal/resources/brokers"
|
|
"github.com/formancehq/operator/internal/resources/brokertopics"
|
|
"github.com/formancehq/operator/internal/resources/caddy"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
|
|
"github.com/formancehq/operator/api/formance.com/v1beta1"
|
|
"github.com/formancehq/operator/internal/core"
|
|
"github.com/formancehq/operator/internal/resources/applications"
|
|
"github.com/formancehq/operator/internal/resources/auths"
|
|
"github.com/formancehq/operator/internal/resources/databases"
|
|
"github.com/formancehq/operator/internal/resources/gateways"
|
|
"github.com/formancehq/operator/internal/resources/jobs"
|
|
"github.com/formancehq/operator/internal/resources/registries"
|
|
"github.com/formancehq/operator/internal/resources/services"
|
|
"github.com/formancehq/operator/internal/resources/settings"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
const (
|
|
ConditionTypeDeploymentStrategy = "LedgerDeploymentStrategy"
|
|
ReasonLedgerSingle = "Single"
|
|
ReasonLedgerMonoWriterMultipleReader = "MonoWriterMultipleReader"
|
|
)
|
|
|
|
func hasDeploymentStrategyChanged(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, strategy string) (err error) {
|
|
condition := v1beta1.NewCondition(ConditionTypeDeploymentStrategy, ledger.Generation).SetReason(
|
|
func() string {
|
|
switch strategy {
|
|
case v1beta1.DeploymentStrategySingle:
|
|
return ReasonLedgerSingle
|
|
case v1beta1.DeploymentStrategyMonoWriterMultipleReader:
|
|
return ReasonLedgerMonoWriterMultipleReader
|
|
default:
|
|
return "unknown strategy"
|
|
}
|
|
}(),
|
|
).SetMessage("Deployment strategy initialized")
|
|
|
|
defer func() {
|
|
ledger.GetConditions().AppendOrReplace(*condition, v1beta1.AndConditions(
|
|
v1beta1.ConditionTypeMatch(ConditionTypeDeploymentStrategy),
|
|
v1beta1.ConditionGenerationMatch(ledger.Generation),
|
|
))
|
|
}()
|
|
|
|
// There is no generation 0, so we can't check for a change in strategy
|
|
// Uninstall is useless if the ledger deployment strategy has not changed
|
|
if ledger.GetConditions().Check(v1beta1.AndConditions(
|
|
v1beta1.ConditionTypeMatch(ConditionTypeDeploymentStrategy),
|
|
v1beta1.ConditionReasonMatch(condition.Reason),
|
|
v1beta1.ConditionGenerationMatch(ledger.Generation-1),
|
|
)) || ledger.GetGeneration() == 1 {
|
|
return
|
|
}
|
|
|
|
condition.SetMessage("Deployment strategy has changed")
|
|
switch strategy {
|
|
case v1beta1.DeploymentStrategySingle:
|
|
return uninstallLedgerMonoWriterMultipleReader(ctx, stack)
|
|
case v1beta1.DeploymentStrategyMonoWriterMultipleReader:
|
|
return core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, "ledger"))
|
|
default:
|
|
return fmt.Errorf("unknown deployment strategy %s", strategy)
|
|
}
|
|
}
|
|
|
|
func installLedger(ctx core.Context, stack *v1beta1.Stack,
|
|
ledger *v1beta1.Ledger, database *v1beta1.Database, image string, isV2 bool) (err error) {
|
|
|
|
deploymentStrategySettings, err := settings.GetStringOrDefault(ctx, stack.Name, v1beta1.DeploymentStrategySingle, "ledger", "deployment-strategy")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ledger.Spec.DeploymentStrategy == v1beta1.DeploymentStrategyMonoWriterMultipleReader {
|
|
deploymentStrategySettings = v1beta1.DeploymentStrategyMonoWriterMultipleReader
|
|
}
|
|
|
|
if err = hasDeploymentStrategyChanged(ctx, stack, ledger, deploymentStrategySettings); err != nil {
|
|
return
|
|
}
|
|
|
|
switch deploymentStrategySettings {
|
|
case v1beta1.DeploymentStrategySingle:
|
|
return installLedgerSingleInstance(ctx, stack, ledger, database, image, isV2)
|
|
case v1beta1.DeploymentStrategyMonoWriterMultipleReader:
|
|
return installLedgerMonoWriterMultipleReader(ctx, stack, ledger, database, image, isV2)
|
|
default:
|
|
return fmt.Errorf("unknown deployment strategy %s", deploymentStrategySettings)
|
|
}
|
|
}
|
|
|
|
func installLedgerSingleInstance(ctx core.Context, stack *v1beta1.Stack,
|
|
ledger *v1beta1.Ledger, database *v1beta1.Database, version string, v2 bool) error {
|
|
container, err := createLedgerContainerFull(ctx, stack, v2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = setCommonContainerConfiguration(ctx, stack, ledger, version, database, container, v2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !v2 && ledger.Spec.Locking != nil && ledger.Spec.Locking.Strategy == "redis" {
|
|
container.Env = append(container.Env,
|
|
core.Env("NUMARY_LOCK_STRATEGY", "redis"),
|
|
core.Env("NUMARY_LOCK_STRATEGY_REDIS_URL", ledger.Spec.Locking.Redis.Uri),
|
|
core.Env("NUMARY_LOCK_STRATEGY_REDIS_TLS_ENABLED", strconv.FormatBool(ledger.Spec.Locking.Redis.TLS)),
|
|
core.Env("NUMARY_LOCK_STRATEGY_REDIS_TLS_INSECURE", strconv.FormatBool(ledger.Spec.Locking.Redis.InsecureTLS)),
|
|
)
|
|
|
|
if ledger.Spec.Locking.Redis.Duration != 0 {
|
|
container.Env = append(container.Env, core.Env("NUMARY_LOCK_STRATEGY_REDIS_DURATION", ledger.Spec.Locking.Redis.Duration.String()))
|
|
}
|
|
|
|
if ledger.Spec.Locking.Redis.Retry != 0 {
|
|
container.Env = append(container.Env, core.Env("NUMARY_LOCK_STRATEGY_REDIS_RETRY", ledger.Spec.Locking.Redis.Retry.String()))
|
|
}
|
|
}
|
|
|
|
if err := createDeployment(ctx, stack, ledger, "ledger", *container, v2, 1); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getUpgradeContainer(ctx core.Context, stack *v1beta1.Stack, database *v1beta1.Database, image, version string) (corev1.Container, error) {
|
|
return databases.MigrateDatabaseContainer(ctx, stack, image, database,
|
|
func(m *databases.MigrationConfiguration) {
|
|
if core.IsLower(version, "v2.0.0-rc.6") {
|
|
m.Command = []string{"buckets", "upgrade-all"}
|
|
}
|
|
m.AdditionalEnv = []corev1.EnvVar{
|
|
core.Env("STORAGE_POSTGRES_CONN_STRING", "$(POSTGRES_URI)"),
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
func installLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, database *v1beta1.Database, image string, v2 bool) error {
|
|
|
|
createDeployment := func(name string, container corev1.Container, replicas uint64) error {
|
|
err := setCommonContainerConfiguration(ctx, stack, ledger, image, database, &container, v2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createDeployment(ctx, stack, ledger, name, container, v2, replicas); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := services.Create(ctx, ledger, name, services.WithDefault(name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
container, err := createLedgerContainerWriteOnly(ctx, stack, v2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := createDeployment("ledger-write", *container, 1); err != nil {
|
|
return err
|
|
}
|
|
|
|
container = createLedgerContainerReadOnly(v2)
|
|
if err := createDeployment("ledger-read", *container, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := createGatewayDeployment(ctx, stack, ledger); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func uninstallLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.Stack) error {
|
|
|
|
remove := func(name string) error {
|
|
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
|
|
return err
|
|
}
|
|
if err := core.DeleteIfExists[*corev1.Service](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := remove("ledger-write"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := remove("ledger-read"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, "ledger-gateway")); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createDeployment(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, name string, container corev1.Container, v2 bool, replicas uint64) error {
|
|
serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var volumes []corev1.Volume
|
|
if !v2 {
|
|
volumes = []corev1.Volume{{
|
|
Name: "config",
|
|
VolumeSource: corev1.VolumeSource{
|
|
EmptyDir: &corev1.EmptyDirVolumeSource{},
|
|
},
|
|
}}
|
|
}
|
|
|
|
tpl := &appsv1.Deployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
Spec: appsv1.DeploymentSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{container},
|
|
Volumes: volumes,
|
|
ServiceAccountName: serviceAccountName,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return applications.
|
|
New(ledger, tpl).
|
|
WithStateful(replicas > 0).
|
|
Install(ctx)
|
|
}
|
|
|
|
func setCommonContainerConfiguration(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, image string, database *v1beta1.Database, container *corev1.Container, v2 bool) error {
|
|
|
|
prefix := ""
|
|
if !v2 {
|
|
prefix = "NUMARY_"
|
|
}
|
|
env := make([]corev1.EnvVar, 0)
|
|
otlpEnv, err := settings.GetOTELEnvVarsWithPrefix(ctx, stack.Name, core.LowerCamelCaseKind(ctx, ledger), prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env = append(env, otlpEnv...)
|
|
|
|
gatewayEnv, err := gateways.EnvVarsIfEnabledWithPrefix(ctx, stack.Name, prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env = append(env, gatewayEnv...)
|
|
env = append(env, core.GetDevEnvVarsWithPrefix(stack, ledger, prefix)...)
|
|
|
|
authEnvVars, err := auths.ProtectedAPIEnvVarsWithPrefix(ctx, stack, "ledger", ledger.Spec.Auth, prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env = append(env, authEnvVars...)
|
|
|
|
postgresEnvVar, err := databases.PostgresEnvVarsWithPrefix(ctx, stack, database, prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
env = append(env, postgresEnvVar...)
|
|
|
|
container.Image = image
|
|
container.Env = append(container.Env, env...)
|
|
container.Env = append(container.Env, core.Env(fmt.Sprintf("%sSTORAGE_POSTGRES_CONN_STRING", prefix), fmt.Sprintf("$(%sPOSTGRES_URI)", prefix)))
|
|
container.Env = append(container.Env, core.Env(fmt.Sprintf("%sSTORAGE_DRIVER", prefix), "postgres"))
|
|
container.Ports = []corev1.ContainerPort{applications.StandardHTTPPort()}
|
|
container.LivenessProbe = applications.DefaultLiveness("http")
|
|
|
|
return nil
|
|
}
|
|
|
|
func createBaseLedgerContainer(v2 bool) *corev1.Container {
|
|
ret := &corev1.Container{
|
|
Name: "ledger",
|
|
}
|
|
var bindFlag = "BIND"
|
|
if !v2 {
|
|
bindFlag = "NUMARY_SERVER_HTTP_BIND_ADDRESS"
|
|
}
|
|
ret.Env = append(ret.Env, core.Env(bindFlag, ":8080"))
|
|
if !v2 {
|
|
ret.VolumeMounts = []corev1.VolumeMount{{
|
|
Name: "config",
|
|
ReadOnly: false,
|
|
MountPath: "/root/.numary",
|
|
}}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func createLedgerContainerFull(ctx core.Context, stack *v1beta1.Stack, v2 bool) (*corev1.Container, error) {
|
|
container := createBaseLedgerContainer(v2)
|
|
|
|
var broker *v1beta1.Broker
|
|
if t, err := brokertopics.Find(ctx, stack, "ledger"); err != nil {
|
|
return nil, err
|
|
} else if t != nil && t.Status.Ready {
|
|
broker = &v1beta1.Broker{}
|
|
if err := ctx.GetClient().Get(ctx, types.NamespacedName{
|
|
Name: stack.Name,
|
|
}, broker); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if broker != nil {
|
|
if !broker.Status.Ready {
|
|
return nil, core.NewPendingError().WithMessage("broker not ready")
|
|
}
|
|
prefix := ""
|
|
if !v2 {
|
|
prefix = "NUMARY_"
|
|
}
|
|
|
|
brokerEnvVar, err := brokers.GetEnvVarsWithPrefix(ctx, broker.Status.URI, stack.Name, "ledger", prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
container.Env = append(container.Env, brokerEnvVar...)
|
|
container.Env = append(container.Env, brokers.GetPublisherEnvVars(stack, broker, "ledger", prefix)...)
|
|
}
|
|
|
|
if v2 {
|
|
hasDependency, err := core.HasDependency(ctx, stack.Name, &v1beta1.Analytics{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasDependency {
|
|
container.Env = append(container.Env, core.Env("EMIT_LOGS", "true"))
|
|
}
|
|
|
|
logsBatchSize, err := settings.GetInt(ctx, stack.Name, "ledger", "logs", "max-batch-size")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if logsBatchSize != nil && *logsBatchSize != 0 {
|
|
container.Env = append(container.Env, core.Env("LEDGER_BATCH_SIZE", fmt.Sprint(*logsBatchSize)))
|
|
}
|
|
}
|
|
|
|
return container, nil
|
|
}
|
|
|
|
func createLedgerContainerWriteOnly(ctx core.Context, stack *v1beta1.Stack, v2 bool) (*corev1.Container, error) {
|
|
return createLedgerContainerFull(ctx, stack, v2)
|
|
}
|
|
|
|
func createLedgerContainerReadOnly(v2 bool) *corev1.Container {
|
|
container := createBaseLedgerContainer(v2)
|
|
container.Env = append(container.Env, core.Env("READ_ONLY", "true"))
|
|
return container
|
|
}
|
|
|
|
func createGatewayDeployment(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger) error {
|
|
|
|
caddyfileConfigMap, err := caddy.CreateCaddyfileConfigMap(ctx, stack, "ledger", Caddyfile, map[string]any{
|
|
"Debug": stack.Spec.Debug || ledger.Spec.Debug,
|
|
}, core.WithController[*corev1.ConfigMap](ctx.GetScheme(), ledger))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
env := make([]corev1.EnvVar, 0)
|
|
env = append(env, core.GetDevEnvVars(stack, ledger)...)
|
|
|
|
caddyImage, err := registries.GetCaddyImage(ctx, stack, "2.7.6-alpine")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpl, err := caddy.DeploymentTemplate(ctx, stack, ledger, caddyfileConfigMap, caddyImage, env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpl.Name = "ledger-gateway"
|
|
return applications.
|
|
New(ledger, tpl).
|
|
Install(ctx)
|
|
}
|
|
|
|
func migrate(ctx core.Context, stack *v1beta1.Stack, ledger *v1beta1.Ledger, database *v1beta1.Database, image, version string) error {
|
|
serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
upgradeContainer, err := getUpgradeContainer(ctx, stack, database, image, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return jobs.Handle(ctx, ledger, "migrate-v2", upgradeContainer,
|
|
jobs.PreCreate(func() error {
|
|
list := &appsv1.DeploymentList{}
|
|
if err := ctx.GetClient().List(ctx, list, client.InNamespace(stack.Name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, item := range list.Items {
|
|
if controller := metav1.GetControllerOf(&item); controller != nil && controller.UID == ledger.GetUID() {
|
|
if err := ctx.GetClient().Delete(ctx, &item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}),
|
|
jobs.WithServiceAccount(serviceAccountName),
|
|
)
|
|
}
|