From 6f7ea2bf98f540b783224ece2850ae00a9e30707 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 11 Nov 2025 16:20:34 +0100 Subject: [PATCH] payment orchestrator build --- .woodpecker/payments_orchestrator.yml | 73 +++++ api/payments/orchestrator/config.yml | 58 ++++ api/payments/orchestrator/go.mod | 3 + api/payments/orchestrator/go.sum | 3 + .../internal/appversion/version.go | 28 ++ .../internal/server/internal/serverimp.go | 298 ++++++++++++++++++ .../orchestrator/internal/server/server.go | 12 + .../internal/service/orchestrator/helpers.go | 2 +- api/payments/orchestrator/main.go | 17 + ci/prod/.env.runtime | 14 + .../compose/payments_orchestrator.dockerfile | 40 +++ ci/prod/compose/payments_orchestrator.yml | 46 +++ .../scripts/deploy/payments_orchestrator.sh | 147 +++++++++ .../payments_orchestrator/build-image.sh | 85 +++++ ci/scripts/payments_orchestrator/deploy.sh | 61 ++++ 15 files changed, 886 insertions(+), 1 deletion(-) create mode 100644 .woodpecker/payments_orchestrator.yml create mode 100644 api/payments/orchestrator/config.yml create mode 100644 api/payments/orchestrator/internal/appversion/version.go create mode 100644 api/payments/orchestrator/internal/server/internal/serverimp.go create mode 100644 api/payments/orchestrator/internal/server/server.go create mode 100644 api/payments/orchestrator/main.go create mode 100644 ci/prod/compose/payments_orchestrator.dockerfile create mode 100644 ci/prod/compose/payments_orchestrator.yml create mode 100755 ci/prod/scripts/deploy/payments_orchestrator.sh create mode 100755 ci/scripts/payments_orchestrator/build-image.sh create mode 100755 ci/scripts/payments_orchestrator/deploy.sh diff --git a/.woodpecker/payments_orchestrator.yml b/.woodpecker/payments_orchestrator.yml new file mode 100644 index 0000000..58697c3 --- /dev/null +++ b/.woodpecker/payments_orchestrator.yml @@ -0,0 +1,73 @@ +matrix: + include: + - PAYMENTS_IMAGE_PATH: payments/orchestrator + PAYMENTS_DOCKERFILE: ci/prod/compose/payments_orchestrator.dockerfile + PAYMENTS_MONGO_SECRET_PATH: sendico/db + PAYMENTS_ENV: prod + +when: + - event: push + branch: main + +steps: + - name: version + image: alpine:latest + commands: + - set -euo pipefail 2>/dev/null || set -eu + - apk add --no-cache git + - GIT_REV="$(git rev-parse --short HEAD)" + - BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)" + - APP_V="$(cat version)" + - BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + - BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}" + - printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \ + "$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version + + - name: proto + image: golang:alpine + depends_on: [ version ] + commands: + - set -eu + - apk add --no-cache bash git build-base protoc protobuf-dev + - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + - export PATH="$(go env GOPATH)/bin:$PATH" + - bash ci/scripts/proto/generate.sh + + - name: secrets + image: alpine:latest + depends_on: [ version ] + environment: + VAULT_ADDR: { from_secret: VAULT_ADDR } + VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE } + VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID } + commands: + - set -euo pipefail + - apk add --no-cache bash coreutils openssh-keygen curl sed python3 + - mkdir -p secrets + - ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600 + - base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY + - chmod 600 secrets/SSH_KEY + - ssh-keygen -y -f secrets/SSH_KEY >/dev/null + - ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER + - ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD + + - name: build-image + image: gcr.io/kaniko-project/executor:debug + depends_on: [ proto, secrets ] + commands: + - sh ci/scripts/payments_orchestrator/build-image.sh + + - name: deploy + image: alpine:latest + depends_on: [ secrets, build-image ] + environment: + VAULT_ADDR: { from_secret: VAULT_ADDR } + VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE } + VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID } + commands: + - set -euo pipefail + - apk add --no-cache bash openssh-client rsync coreutils curl sed python3 + - mkdir -p /root/.ssh + - install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa + - sh ci/scripts/payments_orchestrator/deploy.sh diff --git a/api/payments/orchestrator/config.yml b/api/payments/orchestrator/config.yml new file mode 100644 index 0000000..037c328 --- /dev/null +++ b/api/payments/orchestrator/config.yml @@ -0,0 +1,58 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50062" + enable_reflection: true + enable_health: true + +metrics: + address: ":9403" + +database: + driver: mongodb + settings: + host_env: PAYMENTS_MONGO_HOST + port_env: PAYMENTS_MONGO_PORT + database_env: PAYMENTS_MONGO_DATABASE + user_env: PAYMENTS_MONGO_USER + password_env: PAYMENTS_MONGO_PASSWORD + auth_source_env: PAYMENTS_MONGO_AUTH_SOURCE + replica_set_env: PAYMENTS_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: Payments Orchestrator Service + max_reconnects: 10 + reconnect_wait: 5 + +fees: + address: "sendico_billing_fees:50060" + dial_timeout_seconds: 5 + call_timeout_seconds: 3 + insecure: true + +ledger: + address: "sendico_ledger:50052" + dial_timeout_seconds: 5 + call_timeout_seconds: 3 + insecure: true + +gateway: + address: "sendico_chain_gateway:50054" + dial_timeout_seconds: 5 + call_timeout_seconds: 3 + insecure: true + +oracle: + address: "sendico_fx_oracle:50051" + dial_timeout_seconds: 5 + call_timeout_seconds: 3 + insecure: true diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index 7931eff..de55615 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -23,6 +23,7 @@ require ( go.uber.org/zap v1.27.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -36,6 +37,8 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index b4e0240..1023043 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -65,6 +65,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -195,6 +197,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/api/payments/orchestrator/internal/appversion/version.go b/api/payments/orchestrator/internal/appversion/version.go new file mode 100644 index 0000000..fbf8f4e --- /dev/null +++ b/api/payments/orchestrator/internal/appversion/version.go @@ -0,0 +1,28 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information populated via ldflags. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +// Create returns a printer configured for the payment orchestrator service. +func Create() version.Printer { + vi := version.Info{ + Program: "Sendico Payment Orchestrator Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&vi) +} diff --git a/api/payments/orchestrator/internal/server/internal/serverimp.go b/api/payments/orchestrator/internal/server/internal/serverimp.go new file mode 100644 index 0000000..6280958 --- /dev/null +++ b/api/payments/orchestrator/internal/server/internal/serverimp.go @@ -0,0 +1,298 @@ +package serverimp + +import ( + "context" + "crypto/tls" + "os" + "strings" + "time" + + chainclient "github.com/tech/sendico/chain/gateway/client" + oracleclient "github.com/tech/sendico/fx/oracle/client" + ledgerclient "github.com/tech/sendico/ledger/client" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator" + "github.com/tech/sendico/payments/orchestrator/storage" + mongostorage "github.com/tech/sendico/payments/orchestrator/storage/mongo" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "gopkg.in/yaml.v3" +) + +type Imp struct { + logger mlogger.Logger + file string + debug bool + + config *config + app *grpcapp.App[storage.Repository] + feesConn *grpc.ClientConn + ledgerClient ledgerclient.Client + gatewayClient chainclient.Client + oracleClient oracleclient.Client +} + +type config struct { + *grpcapp.Config `yaml:",inline"` + Fees clientConfig `yaml:"fees"` + Ledger clientConfig `yaml:"ledger"` + Gateway clientConfig `yaml:"gateway"` + Oracle clientConfig `yaml:"oracle"` +} + +type clientConfig struct { + Address string `yaml:"address"` + DialTimeoutSecs int `yaml:"dial_timeout_seconds"` + CallTimeoutSecs int `yaml:"call_timeout_seconds"` + InsecureTransport bool `yaml:"insecure"` +} + +func (c clientConfig) address() string { + return strings.TrimSpace(c.Address) +} + +func (c clientConfig) dialTimeout() time.Duration { + if c.DialTimeoutSecs <= 0 { + return 5 * time.Second + } + return time.Duration(c.DialTimeoutSecs) * time.Second +} + +func (c clientConfig) callTimeout() time.Duration { + if c.CallTimeoutSecs <= 0 { + return 3 * time.Second + } + return time.Duration(c.CallTimeoutSecs) * time.Second +} + +func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { + return &Imp{ + logger: logger.Named("server"), + file: file, + debug: debug, + }, nil +} + +func (i *Imp) Shutdown() { + if i.app != nil { + timeout := 15 * time.Second + if i.config != nil && i.config.Runtime != nil { + timeout = i.config.Runtime.ShutdownTimeout() + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + i.app.Shutdown(ctx) + cancel() + } + + if i.ledgerClient != nil { + _ = i.ledgerClient.Close() + } + if i.gatewayClient != nil { + _ = i.gatewayClient.Close() + } + if i.oracleClient != nil { + _ = i.oracleClient.Close() + } + if i.feesConn != nil { + _ = i.feesConn.Close() + } +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return mongostorage.New(logger, conn) + } + + feesClient, feesConn := i.initFeesClient(cfg.Fees) + if feesConn != nil { + i.feesConn = feesConn + } + + ledgerClient := i.initLedgerClient(cfg.Ledger) + if ledgerClient != nil { + i.ledgerClient = ledgerClient + } + + gatewayClient := i.initGatewayClient(cfg.Gateway) + if gatewayClient != nil { + i.gatewayClient = gatewayClient + } + + oracleClient := i.initOracleClient(cfg.Oracle) + if oracleClient != nil { + i.oracleClient = oracleClient + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + opts := []orchestrator.Option{} + if feesClient != nil { + opts = append(opts, orchestrator.WithFeeEngine(feesClient, cfg.Fees.callTimeout())) + } + if ledgerClient != nil { + opts = append(opts, orchestrator.WithLedgerClient(ledgerClient)) + } + if gatewayClient != nil { + opts = append(opts, orchestrator.WithChainGatewayClient(gatewayClient)) + } + if oracleClient != nil { + opts = append(opts, orchestrator.WithOracleClient(oracleClient)) + } + return orchestrator.NewService(logger, repo, opts...), nil + } + + app, err := grpcapp.NewApp(i.logger, "payments_orchestrator", cfg.Config, i.debug, repoFactory, serviceFactory) + if err != nil { + return err + } + i.app = app + + return i.app.Start() +} + +func (i *Imp) initFeesClient(cfg clientConfig) (feesv1.FeeEngineClient, *grpc.ClientConn) { + addr := cfg.address() + if addr == "" { + return nil, nil + } + + dialCtx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout()) + defer cancel() + + creds := credentials.NewTLS(&tls.Config{}) + if cfg.InsecureTransport { + creds = insecure.NewCredentials() + } + + conn, err := grpc.DialContext(dialCtx, addr, grpc.WithTransportCredentials(creds)) + if err != nil { + i.logger.Warn("failed to connect to fees service", zap.String("address", addr), zap.Error(err)) + return nil, nil + } + + i.logger.Info("connected to fees service", zap.String("address", addr)) + return feesv1.NewFeeEngineClient(conn), conn +} + +func (i *Imp) initLedgerClient(cfg clientConfig) ledgerclient.Client { + addr := cfg.address() + if addr == "" { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout()) + defer cancel() + + client, err := ledgerclient.New(ctx, ledgerclient.Config{ + Address: addr, + DialTimeout: cfg.dialTimeout(), + CallTimeout: cfg.callTimeout(), + Insecure: cfg.InsecureTransport, + }) + if err != nil { + i.logger.Warn("failed to connect to ledger service", zap.String("address", addr), zap.Error(err)) + return nil + } + i.logger.Info("connected to ledger service", zap.String("address", addr)) + return client +} + +func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client { + addr := cfg.address() + if addr == "" { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout()) + defer cancel() + + client, err := chainclient.New(ctx, chainclient.Config{ + Address: addr, + DialTimeout: cfg.dialTimeout(), + CallTimeout: cfg.callTimeout(), + Insecure: cfg.InsecureTransport, + }) + if err != nil { + i.logger.Warn("failed to connect to chain gateway service", zap.String("address", addr), zap.Error(err)) + return nil + } + i.logger.Info("connected to chain gateway service", zap.String("address", addr)) + return client +} + +func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client { + addr := cfg.address() + if addr == "" { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout()) + defer cancel() + + client, err := oracleclient.New(ctx, oracleclient.Config{ + Address: addr, + DialTimeout: cfg.dialTimeout(), + CallTimeout: cfg.callTimeout(), + Insecure: cfg.InsecureTransport, + }) + if err != nil { + i.logger.Warn("failed to connect to oracle service", zap.String("address", addr), zap.Error(err)) + return nil + } + i.logger.Info("connected to oracle service", zap.String("address", addr)) + return client +} + +func (i *Imp) loadConfig() (*config, error) { + data, err := os.ReadFile(i.file) + if err != nil { + i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) + return nil, err + } + + cfg := &config{Config: &grpcapp.Config{}} + if err := yaml.Unmarshal(data, cfg); err != nil { + i.logger.Error("Failed to parse configuration", zap.Error(err)) + return nil, err + } + + if cfg.Runtime == nil { + cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} + } + + if cfg.GRPC == nil { + cfg.GRPC = &routers.GRPCConfig{ + Network: "tcp", + Address: ":50062", + EnableReflection: true, + EnableHealth: true, + } + } else { + if strings.TrimSpace(cfg.GRPC.Address) == "" { + cfg.GRPC.Address = ":50062" + } + if strings.TrimSpace(cfg.GRPC.Network) == "" { + cfg.GRPC.Network = "tcp" + } + } + + if cfg.Metrics == nil { + cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9403"} + } else if strings.TrimSpace(cfg.Metrics.Address) == "" { + cfg.Metrics.Address = ":9403" + } + + return cfg, nil +} diff --git a/api/payments/orchestrator/internal/server/server.go b/api/payments/orchestrator/internal/server/server.go new file mode 100644 index 0000000..2d6d95a --- /dev/null +++ b/api/payments/orchestrator/internal/server/server.go @@ -0,0 +1,12 @@ +package server + +import ( + serverimp "github.com/tech/sendico/payments/orchestrator/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +// Create initialises the payment orchestrator server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/helpers.go b/api/payments/orchestrator/internal/service/orchestrator/helpers.go index 895e32f..49f9c66 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/helpers.go +++ b/api/payments/orchestrator/internal/service/orchestrator/helpers.go @@ -3,12 +3,12 @@ package orchestrator import ( "strings" + "github.com/shopspring/decimal" oracleclient "github.com/tech/sendico/fx/oracle/client" "github.com/tech/sendico/pkg/merrors" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" - "github.com/shopspring/decimal" "google.golang.org/protobuf/proto" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" diff --git a/api/payments/orchestrator/main.go b/api/payments/orchestrator/main.go new file mode 100644 index 0000000..6528eaf --- /dev/null +++ b/api/payments/orchestrator/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/payments/orchestrator/internal/appversion" + si "github.com/tech/sendico/payments/orchestrator/internal/server" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" + smain "github.com/tech/sendico/pkg/server/main" +) + +func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return si.Create(logger, file, debug) +} + +func main() { + smain.RunServer("main", appversion.Create(), factory) +} diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index d608112..b636292 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -73,3 +73,17 @@ LEDGER_MONGO_PORT=27017 LEDGER_MONGO_DATABASE=ledger LEDGER_MONGO_AUTH_SOURCE=admin LEDGER_MONGO_REPLICA_SET=sendico-rs + +# Payments orchestrator stack +PAYMENTS_DIR=payments_orchestrator +PAYMENTS_COMPOSE_PROJECT=sendico-payments-orchestrator +PAYMENTS_SERVICE_NAME=sendico_payments_orchestrator +PAYMENTS_GRPC_PORT=50062 +PAYMENTS_METRICS_PORT=9403 + +# Payments orchestrator Mongo settings +PAYMENTS_MONGO_HOST=sendico_db1 +PAYMENTS_MONGO_PORT=27017 +PAYMENTS_MONGO_DATABASE=payments_orchestrator +PAYMENTS_MONGO_AUTH_SOURCE=admin +PAYMENTS_MONGO_REPLICA_SET=sendico-rs diff --git a/ci/prod/compose/payments_orchestrator.dockerfile b/ci/prod/compose/payments_orchestrator.dockerfile new file mode 100644 index 0000000..51b41fc --- /dev/null +++ b/ci/prod/compose/payments_orchestrator.dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1.7 + +ARG TARGETOS=linux +ARG TARGETARCH=amd64 + +FROM golang:alpine AS build +ARG APP_VERSION=dev +ARG GIT_REV=unknown +ARG BUILD_BRANCH=unknown +ARG BUILD_DATE=unknown +ARG BUILD_USER=ci +ENV GO111MODULE=on +ENV PATH="/go/bin:${PATH}" +WORKDIR /src +COPY . . +RUN apk add --no-cache bash git build-base protoc protobuf-dev \ + && go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \ + && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \ + && bash ci/scripts/proto/generate.sh +WORKDIR /src/api/payments/orchestrator +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags "\ + -s -w \ + -X github.com/tech/sendico/payments/orchestrator/internal/appversion.Version=${APP_VERSION} \ + -X github.com/tech/sendico/payments/orchestrator/internal/appversion.Revision=${GIT_REV} \ + -X github.com/tech/sendico/payments/orchestrator/internal/appversion.Branch=${BUILD_BRANCH} \ + -X github.com/tech/sendico/payments/orchestrator/internal/appversion.BuildUser=${BUILD_USER} \ + -X github.com/tech/sendico/payments/orchestrator/internal/appversion.BuildDate=${BUILD_DATE}" \ + -o /out/payments-orchestrator . + +FROM alpine:latest AS runtime +RUN apk add --no-cache ca-certificates tzdata wget +WORKDIR /app +COPY api/payments/orchestrator/config.yml /app/config.yml +COPY --from=build /out/payments-orchestrator /app/payments-orchestrator +EXPOSE 50062 9403 +ENTRYPOINT ["/app/payments-orchestrator"] +CMD ["--config.file", "/app/config.yml"] diff --git a/ci/prod/compose/payments_orchestrator.yml b/ci/prod/compose/payments_orchestrator.yml new file mode 100644 index 0000000..6547f06 --- /dev/null +++ b/ci/prod/compose/payments_orchestrator.yml @@ -0,0 +1,46 @@ +# Compose v2 - Payments Orchestrator + +x-common-env: &common-env + env_file: + - ../env/.env.runtime + - ../env/.env.version + +networks: + sendico-net: + external: true + name: sendico-net + +services: + sendico_payments_orchestrator: + <<: *common-env + container_name: sendico-payments-orchestrator + restart: unless-stopped + image: ${REGISTRY_URL}/payments/orchestrator:${APP_V} + pull_policy: always + environment: + PAYMENTS_MONGO_HOST: ${PAYMENTS_MONGO_HOST} + PAYMENTS_MONGO_PORT: ${PAYMENTS_MONGO_PORT} + PAYMENTS_MONGO_DATABASE: ${PAYMENTS_MONGO_DATABASE} + PAYMENTS_MONGO_USER: ${PAYMENTS_MONGO_USER} + PAYMENTS_MONGO_PASSWORD: ${PAYMENTS_MONGO_PASSWORD} + PAYMENTS_MONGO_AUTH_SOURCE: ${PAYMENTS_MONGO_AUTH_SOURCE} + PAYMENTS_MONGO_REPLICA_SET: ${PAYMENTS_MONGO_REPLICA_SET} + NATS_URL: ${NATS_URL} + NATS_HOST: ${NATS_HOST} + NATS_PORT: ${NATS_PORT} + NATS_USER: ${NATS_USER} + NATS_PASSWORD: ${NATS_PASSWORD} + PAYMENTS_GRPC_PORT: ${PAYMENTS_GRPC_PORT} + PAYMENTS_METRICS_PORT: ${PAYMENTS_METRICS_PORT} + command: ["--config.file", "/app/config.yml"] + ports: + - "0.0.0.0:${PAYMENTS_GRPC_PORT}:50062" + - "0.0.0.0:${PAYMENTS_METRICS_PORT}:${PAYMENTS_METRICS_PORT}" + healthcheck: + test: ["CMD-SHELL","wget -qO- http://localhost:${PAYMENTS_METRICS_PORT}/health | grep -q '\"status\":\"ok\"'"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + networks: + - sendico-net diff --git a/ci/prod/scripts/deploy/payments_orchestrator.sh b/ci/prod/scripts/deploy/payments_orchestrator.sh new file mode 100755 index 0000000..584ceb5 --- /dev/null +++ b/ci/prod/scripts/deploy/payments_orchestrator.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail +[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && set -x +trap 'echo "[deploy-payments-orchestrator] error at line $LINENO" >&2' ERR + +: "${REMOTE_BASE:?missing REMOTE_BASE}" +: "${SSH_USER:?missing SSH_USER}" +: "${SSH_HOST:?missing SSH_HOST}" +: "${PAYMENTS_DIR:?missing PAYMENTS_DIR}" +: "${PAYMENTS_COMPOSE_PROJECT:?missing PAYMENTS_COMPOSE_PROJECT}" +: "${PAYMENTS_SERVICE_NAME:?missing PAYMENTS_SERVICE_NAME}" + +REMOTE_DIR="${REMOTE_BASE%/}/${PAYMENTS_DIR}" +REMOTE_TARGET="${SSH_USER}@${SSH_HOST}" +COMPOSE_FILE="payments_orchestrator.yml" +SERVICE_NAMES="${PAYMENTS_SERVICE_NAME}" + +REQUIRED_SECRETS=( + PAYMENTS_MONGO_USER + PAYMENTS_MONGO_PASSWORD + NATS_USER + NATS_PASSWORD + NATS_URL +) + +for var in "${REQUIRED_SECRETS[@]}"; do + if [[ -z "${!var:-}" ]]; then + echo "missing required secret env: ${var}" >&2 + exit 65 + fi +done + +if [[ ! -s .env.version ]]; then + echo ".env.version is missing; run version step first" >&2 + exit 66 +fi + +b64enc() { + printf '%s' "$1" | base64 | tr -d '\n' +} + +PAYMENTS_MONGO_USER_B64="$(b64enc "${PAYMENTS_MONGO_USER}")" +PAYMENTS_MONGO_PASSWORD_B64="$(b64enc "${PAYMENTS_MONGO_PASSWORD}")" +NATS_USER_B64="$(b64enc "${NATS_USER}")" +NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" +NATS_URL_B64="$(b64enc "${NATS_URL}")" + +SSH_OPTS=( + -i /root/.ssh/id_rsa + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o LogLevel=ERROR + -q +) +if [[ "${DEBUG_DEPLOY:-0}" = "1" ]]; then + SSH_OPTS=("${SSH_OPTS[@]/-q/}" -vv) +fi + +RSYNC_FLAGS=(-az --delete) +[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && RSYNC_FLAGS=(-avz --delete) + +ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/{compose,env}" + +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/compose/ "$REMOTE_TARGET:${REMOTE_DIR}/compose/" +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/.env.runtime "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.runtime" +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" .env.version "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.version" + +SERVICES_LINE="${SERVICE_NAMES}" + +ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ + REMOTE_DIR="$REMOTE_DIR" \ + COMPOSE_FILE="$COMPOSE_FILE" \ + COMPOSE_PROJECT="$PAYMENTS_COMPOSE_PROJECT" \ + SERVICES_LINE="$SERVICES_LINE" \ + PAYMENTS_MONGO_USER_B64="$PAYMENTS_MONGO_USER_B64" \ + PAYMENTS_MONGO_PASSWORD_B64="$PAYMENTS_MONGO_PASSWORD_B64" \ + NATS_USER_B64="$NATS_USER_B64" \ + NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ + NATS_URL_B64="$NATS_URL_B64" \ + bash -s <<'EOSSH' +set -euo pipefail +cd "${REMOTE_DIR}/compose" +set -a +. ../env/.env.runtime +load_kv_file() { + local file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + if printf '%s' "$line" | grep -Eq '^[[:alpha:]_][[:alnum:]_]*='; then + local key="${line%%=*}" + local value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + if [[ -n "$key" ]]; then + export "$key=$value" + fi + fi + done <"$file" +} +load_kv_file ../env/.env.version +set +a + +if base64 -d >/dev/null 2>&1 <<<'AA=='; then + BASE64_DECODE_FLAG='-d' +else + BASE64_DECODE_FLAG='--decode' +fi + +decode_b64() { + val="$1" + if [[ -z "$val" ]]; then + printf '' + return + fi + printf '%s' "$val" | base64 "${BASE64_DECODE_FLAG}" +} + +PAYMENTS_MONGO_USER="$(decode_b64 "$PAYMENTS_MONGO_USER_B64")" +PAYMENTS_MONGO_PASSWORD="$(decode_b64 "$PAYMENTS_MONGO_PASSWORD_B64")" +NATS_USER="$(decode_b64 "$NATS_USER_B64")" +NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" +NATS_URL="$(decode_b64 "$NATS_URL_B64")" + +export PAYMENTS_MONGO_USER PAYMENTS_MONGO_PASSWORD NATS_USER NATS_PASSWORD NATS_URL +COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" +export COMPOSE_PROJECT_NAME +read -r -a SERVICES <<<"${SERVICES_LINE}" + +pull_cmd=(docker compose -f "$COMPOSE_FILE" pull) +up_cmd=(docker compose -f "$COMPOSE_FILE" up -d --remove-orphans) +ps_cmd=(docker compose -f "$COMPOSE_FILE" ps) +if [[ "${#SERVICES[@]}" -gt 0 ]]; then + pull_cmd+=("${SERVICES[@]}") + up_cmd+=("${SERVICES[@]}") + ps_cmd+=("${SERVICES[@]}") +fi + +"${pull_cmd[@]}" +"${up_cmd[@]}" +"${ps_cmd[@]}" + +date -Is > .last_deploy +logger -t "deploy-${COMPOSE_PROJECT_NAME}" "${COMPOSE_PROJECT_NAME} deployed at $(date -Is) in ${REMOTE_DIR}" +EOSSH diff --git a/ci/scripts/payments_orchestrator/build-image.sh b/ci/scripts/payments_orchestrator/build-image.sh new file mode 100755 index 0000000..b86e318 --- /dev/null +++ b/ci/scripts/payments_orchestrator/build-image.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +if ! set -o pipefail 2>/dev/null; then + : +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +sh ci/scripts/common/ensure_env_version.sh + +normalize_env_file() { + file="$1" + tmp="${file}.tmp.$$" + tr -d '\r' <"$file" >"$tmp" + mv "$tmp" "$file" +} + +load_env_file() { + file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + key="${line%%=*}" + value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + export "$key=$value" + done <"$file" +} + +PAYMENTS_ENV_NAME="${PAYMENTS_ENV:-prod}" +RUNTIME_ENV_FILE="./ci/${PAYMENTS_ENV_NAME}/.env.runtime" + +if [ ! -f "${RUNTIME_ENV_FILE}" ]; then + echo "[payments-orchestrator-build] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2 + exit 1 +fi + +normalize_env_file "${RUNTIME_ENV_FILE}" +normalize_env_file ./.env.version + +load_env_file "${RUNTIME_ENV_FILE}" +load_env_file ./.env.version + +REGISTRY_URL="${REGISTRY_URL:?missing REGISTRY_URL}" +APP_V="${APP_V:?missing APP_V}" +PAYMENTS_DOCKERFILE="${PAYMENTS_DOCKERFILE:?missing PAYMENTS_DOCKERFILE}" +PAYMENTS_IMAGE_PATH="${PAYMENTS_IMAGE_PATH:?missing PAYMENTS_IMAGE_PATH}" + +REGISTRY_HOST="${REGISTRY_URL#http://}" +REGISTRY_HOST="${REGISTRY_HOST#https://}" +REGISTRY_USER="$(cat secrets/REGISTRY_USER)" +REGISTRY_PASSWORD="$(cat secrets/REGISTRY_PASSWORD)" +: "${REGISTRY_USER:?missing registry user}" +: "${REGISTRY_PASSWORD:?missing registry password}" + +mkdir -p /kaniko/.docker +AUTH_B64="$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')" +cat </kaniko/.docker/config.json +{ + "auths": { + "https://${REGISTRY_HOST}": { "auth": "${AUTH_B64}" } + } +} +EOF + +BUILD_CONTEXT="${PAYMENTS_BUILD_CONTEXT:-${WOODPECKER_WORKSPACE:-${CI_WORKSPACE:-${PWD:-/workspace}}}}" +if [ ! -d "${BUILD_CONTEXT}" ]; then + BUILD_CONTEXT="/workspace" +fi + +/kaniko/executor \ + --context "${BUILD_CONTEXT}" \ + --dockerfile "${PAYMENTS_DOCKERFILE}" \ + --destination "${REGISTRY_URL}/${PAYMENTS_IMAGE_PATH}:${APP_V}" \ + --build-arg APP_VERSION="${APP_V}" \ + --build-arg GIT_REV="${GIT_REV}" \ + --build-arg BUILD_BRANCH="${BUILD_BRANCH}" \ + --build-arg BUILD_DATE="${BUILD_DATE}" \ + --build-arg BUILD_USER="${BUILD_USER}" \ + --single-snapshot diff --git a/ci/scripts/payments_orchestrator/deploy.sh b/ci/scripts/payments_orchestrator/deploy.sh new file mode 100755 index 0000000..31b9db3 --- /dev/null +++ b/ci/scripts/payments_orchestrator/deploy.sh @@ -0,0 +1,61 @@ +#!/bin/sh +set -eu + +if ! set -o pipefail 2>/dev/null; then + : +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +sh ci/scripts/common/ensure_env_version.sh + +normalize_env_file() { + file="$1" + tmp="${file}.tmp.$$" + tr -d '\r' <"$file" >"$tmp" + mv "$tmp" "$file" +} + +load_env_file() { + file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + key="${line%%=*}" + value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + export "$key=$value" + done <"$file" +} + +PAYMENTS_ENV_NAME="${PAYMENTS_ENV:-prod}" +RUNTIME_ENV_FILE="./ci/${PAYMENTS_ENV_NAME}/.env.runtime" + +if [ ! -f "${RUNTIME_ENV_FILE}" ]; then + echo "[payments-orchestrator-deploy] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2 + exit 1 +fi + +normalize_env_file "${RUNTIME_ENV_FILE}" +normalize_env_file ./.env.version + +load_env_file "${RUNTIME_ENV_FILE}" +load_env_file ./.env.version + +PAYMENTS_MONGO_SECRET_PATH="${PAYMENTS_MONGO_SECRET_PATH:?missing PAYMENTS_MONGO_SECRET_PATH}" +: "${NATS_HOST:?missing NATS_HOST}" +: "${NATS_PORT:?missing NATS_PORT}" + +export PAYMENTS_MONGO_USER="$(./ci/vlt kv_get kv "${PAYMENTS_MONGO_SECRET_PATH}" user)" +export PAYMENTS_MONGO_PASSWORD="$(./ci/vlt kv_get kv "${PAYMENTS_MONGO_SECRET_PATH}" password)" + +export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)" +export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)" +export NATS_URL="nats://${NATS_USER}:${NATS_PASSWORD}@${NATS_HOST}:${NATS_PORT}" + +bash ci/prod/scripts/bootstrap/network.sh +bash ci/prod/scripts/deploy/payments_orchestrator.sh