diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c914941..ccc1d72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,30 @@ jobs: - name: Vulnerabilidades conhecidas run: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./... + pulumi-component-tests: + name: Componente Pulumi (Go) tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: deploy/pulumi + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: deploy/pulumi/go.mod + cache-dependency-path: deploy/pulumi/go.sum + + - name: Run vet + run: go vet ./... + + - name: Run golangci-lint + run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 run -c ../../markupp/.golangci.yml + + - name: Run tests + run: go test ./... -race -count=1 + convencoes: name: Convencoes de commit runs-on: ubuntu-latest diff --git a/.github/workflows/smoke-dev.yml b/.github/workflows/smoke-dev.yml new file mode 100644 index 0000000..8c9562f --- /dev/null +++ b/.github/workflows/smoke-dev.yml @@ -0,0 +1,49 @@ +name: Smoke da stack dev + +# Testa a implantação em cada PR e a cada push em dev com o mesmo smoke de +# make smoke-kind: kind com os requisitos do cluster de produção, stack dev do +# Pulumi e API testada por https pelo Gateway. + +on: + pull_request: + paths: + - 'markupp/**' + - 'deploy/**' + - '.github/workflows/smoke-dev.yml' + push: + branches: [dev] + paths: + - 'markupp/**' + - 'deploy/**' + - '.github/workflows/smoke-dev.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke-kind: + name: Smoke da stack dev num kind + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: deploy/pulumi/go.mod + cache-dependency-path: deploy/pulumi/go.sum + + - name: Instala kind + run: go install sigs.k8s.io/kind@v0.33.0 + + - name: Instala helm + uses: azure/setup-helm@v5 + with: + version: v4.3.0 + + - name: Instala o Pulumi + uses: pulumi/actions@v7 + + - name: Smoke da stack dev num kind + run: ./deploy/kind/ambiente.sh smoke diff --git a/Makefile b/Makefile index 92e7553..a51f91e 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ DOCKER_COMPOSE=docker compose META_COVERAGE=90 PERFIL_COVERAGE=markupp/cover.out -.PHONY: all test compose-config compose-env docker-up docker-down run hooks test-scripts coverage coverage-check +.PHONY: all test smoke-kind kind-up kind-painel kind-down compose-config compose-env docker-up docker-down run hooks test-scripts coverage coverage-check all: compose-env compose-config test @@ -12,6 +12,23 @@ all: compose-env compose-config test test: cd markupp && go test ./... +# Smoke descartável da stack dev num kind com os requisitos do cluster de produção. +# Precisa de Docker, kind, helm, kubectl e Pulumi, e apaga o cluster no fim +smoke-kind: + ./deploy/kind/ambiente.sh smoke + +# Sobe o mesmo ambiente com o painel Headlamp e deixa de pé para acompanhar +kind-up: + ./deploy/kind/ambiente.sh up + +# Abre o Headlamp do ambiente de pé em http://localhost:4466 +kind-painel: + ./deploy/kind/ambiente.sh painel + +# Apaga o ambiente de pé +kind-down: + ./deploy/kind/ambiente.sh down + # Valida o arquivo docker-compose e a interpolação de variáveis de ambiente compose-config: $(DOCKER_COMPOSE) -f $(COMPOSE_FILE) config diff --git a/deploy/kind/ambiente.sh b/deploy/kind/ambiente.sh new file mode 100755 index 0000000..579ce7d --- /dev/null +++ b/deploy/kind/ambiente.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Ambiente da stack dev num kind com os requisitos do cluster de produção. +# +# up sobe o cluster, o painel Headlamp e a stack dev, e deixa de pé +# painel abre o Headlamp em http://localhost:4466 e mostra o token de acesso +# down apaga o cluster e o estado do Pulumi do ambiente de pé +# smoke sobe, testa a API por https e apaga tudo, passe ou falhe; é o do CI +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +STACKS="$REPO/deploy/stacks" +IMAGE=markupp:dev +URL=https://markupp.local:8443 +AMBIENTE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/markupp-kind" +PAINEL_PORTA=4466 + +export CLUSTER=markupp-dev +export PULUMI_CONFIG_PASSPHRASE="" + +# usa_diretorio guarda kubeconfig e estado do Pulumi em $1, longe do +# ~/.kube/config de quem roda. +usa_diretorio() { + mkdir -p "$1/state" + export KUBECONFIG="$1/kubeconfig" + export PULUMI_BACKEND_URL="file://$1/state" +} + +recusa_cluster_existente() { + if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then + echo "o cluster $CLUSTER já existe; rode make kind-down antes" >&2 + exit 1 + fi +} + +sobe_cluster() { + docker build -q -t "$IMAGE" "$REPO/markupp" >/dev/null + "$REPO/deploy/kind/prepara-cluster.sh" + kind load docker-image "$IMAGE" --name "$CLUSTER" +} + +aplica_stack() { + cd "$STACKS" + pulumi stack select dev --create --non-interactive + pulumi up --yes --non-interactive +} + +instala_painel() { + helm install headlamp headlamp --repo https://kubernetes-sigs.github.io/headlamp/ \ + --namespace headlamp --create-namespace --wait --timeout 10m +} + +curl_api() { + curl -sfk --resolve markupp.local:8443:127.0.0.1 "$@" +} + +testa_api() { + for _ in $(seq 1 30); do + if curl_api "$URL/healthz"; then + echo + curl_api -X POST "$URL/notes" -H 'Content-Type: application/json' -d '{"path":"smoke.md","content":"oi"}' + echo + return 0 + fi + sleep 5 + done + kubectl -n markupp get all,certificate,gateway,httproute + return 1 +} + +cmd_up() { + recusa_cluster_existente + usa_diretorio "$AMBIENTE_DIR" + sobe_cluster + instala_painel + aplica_stack + echo "API de pé: curl -k --resolve markupp.local:8443:127.0.0.1 $URL/healthz" + echo "painel: make kind-painel" +} + +cmd_painel() { + usa_diretorio "$AMBIENTE_DIR" + echo "token para entrar no Headlamp:" + kubectl create token headlamp --namespace headlamp --duration 24h + echo "painel em http://localhost:$PAINEL_PORTA, Ctrl+C para fechar" + kubectl port-forward --namespace headlamp service/headlamp "$PAINEL_PORTA:80" +} + +cmd_down() { + kind delete cluster --name "$CLUSTER" + rm -rf "$AMBIENTE_DIR" +} + +cmd_smoke() { + recusa_cluster_existente + SMOKE_DIR="$(mktemp -d)" + trap 'kind delete cluster --name "$CLUSTER" >/dev/null 2>&1 || true; rm -rf "$SMOKE_DIR"' EXIT + usa_diretorio "$SMOKE_DIR" + sobe_cluster + aplica_stack + testa_api + echo "smoke passou" +} + +case "${1:-}" in + up) cmd_up ;; + painel) cmd_painel ;; + down) cmd_down ;; + smoke) cmd_smoke ;; + *) + echo "subcomando '${1:-}' desconhecido, esperado up, painel, down ou smoke" >&2 + exit 2 + ;; +esac diff --git a/deploy/kind/cilium-values.yaml b/deploy/kind/cilium-values.yaml new file mode 100644 index 0000000..065216f --- /dev/null +++ b/deploy/kind/cilium-values.yaml @@ -0,0 +1,22 @@ +# Espelha o Cilium do cluster de produção: Gateway API com o Envoy na rede +# do nó, sem kube-proxy. O k8sServiceHost é preenchido pelo script, e o +# operator tem uma réplica porque o kind tem um nó só. +kubeProxyReplacement: true +k8sServicePort: 6443 +ipam: + mode: kubernetes +gatewayAPI: + enabled: true + enableAlpn: true + hostNetwork: + enabled: true +envoy: + enabled: true + securityContext: + privileged: true + capabilities: + keepCapNetBindService: true + envoy: + - NET_BIND_SERVICE +operator: + replicas: 1 diff --git a/deploy/kind/cluster.yaml b/deploy/kind/cluster.yaml new file mode 100644 index 0000000..773fb48 --- /dev/null +++ b/deploy/kind/cluster.yaml @@ -0,0 +1,12 @@ +# Cluster da stack dev. Sem CNI nem kube-proxy padrão, porque o Cilium +# substitui os dois, como no cluster de produção. +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +networking: + disableDefaultCNI: true + kubeProxyMode: none +nodes: + - role: control-plane + extraPortMappings: + - containerPort: 443 + hostPort: 8443 diff --git a/deploy/kind/prepara-cluster.sh b/deploy/kind/prepara-cluster.sh new file mode 100755 index 0000000..d9c8e52 --- /dev/null +++ b/deploy/kind/prepara-cluster.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Sobe o cluster kind da stack dev com os pré-requisitos que o cluster de produção +# tem: Gateway API, Cilium e cert-manager. +set -euo pipefail + +CLUSTER="${CLUSTER:-markupp-dev}" +NAMESPACE="${NAMESPACE:-markupp}" +DIR="$(cd "$(dirname "$0")" && pwd)" +GATEWAY_API_VERSION=v1.6.1 +CILIUM_VERSION=1.20.2 +CERT_MANAGER_VERSION=v1.21.2 + +cria_cluster() { + kind create cluster --name "$CLUSTER" --config "$DIR/cluster.yaml" + kubectl apply --server-side -f \ + "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/standard-install.yaml" +} + +instala_cilium() { + helm install cilium cilium --repo https://helm.cilium.io --version "$CILIUM_VERSION" \ + --namespace kube-system --values "$DIR/cilium-values.yaml" \ + --set k8sServiceHost="$CLUSTER-control-plane" --wait --timeout 10m +} + +instala_cert_manager() { + kubectl apply -f \ + "https://github.com/cert-manager/cert-manager/releases/download/$CERT_MANAGER_VERSION/cert-manager.yaml" + liga_gateway_no_cert_manager + kubectl wait --for=condition=Available --timeout=10m deployment --all -n cert-manager +} + +# liga_gateway_no_cert_manager faz o cert-manager emitir certificado a partir +# da anotação do Gateway, como o controller de produção roda com --enable-gateway-api. +liga_gateway_no_cert_manager() { + kubectl -n cert-manager patch deployment cert-manager --type=json -p \ + '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--enable-gateway-api"}]' +} + +cria_cluster +instala_cilium +instala_cert_manager +kubectl create namespace "$NAMESPACE" diff --git a/deploy/pulumi/PulumiPlugin.yaml b/deploy/pulumi/PulumiPlugin.yaml new file mode 100644 index 0000000..9d33323 --- /dev/null +++ b/deploy/pulumi/PulumiPlugin.yaml @@ -0,0 +1 @@ +runtime: go diff --git a/deploy/pulumi/database.go b/deploy/pulumi/database.go new file mode 100644 index 0000000..cd99af2 --- /dev/null +++ b/deploy/pulumi/database.go @@ -0,0 +1,150 @@ +package main + +import ( + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi-random/sdk/v4/go/random" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +const ( + // dbSecretKey é a chave com a URL de conexão que a API e a migração leem. + dbSecretKey = "uri" + dbPasswordKey = "password" + postgresImage = "postgres:17.9-alpine3.23" + postgresPort = 5432 + postgresUser = "markupp" + passwordLength = 32 + postgresDataPath = "/var/lib/postgresql/data" +) + +// declareDatabase cria o Postgres do namespace ou o Secret do banco externo, +// e devolve o recurso de que a migração depende. +func declareDatabase(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) (pulumi.Resource, error) { + if args.Database.ExternalURL != nil { + return declareExternalDatabase(ctx, names, args, parent) + } + return declareNamespacePostgres(ctx, names, args, parent) +} + +// declareExternalDatabase guarda a URL num Secret. O provider Kubernetes já +// trata stringData de Secret como segredo no estado. +func declareExternalDatabase(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) (pulumi.Resource, error) { + return corev1.NewSecret(ctx, names.dbSecret, &corev1.SecretArgs{ + Metadata: namespaced(names.dbSecret, args.Namespace), + StringData: pulumi.StringMap{dbSecretKey: args.Database.ExternalURL}, + }, parent) +} + +// declareNamespacePostgres sobe um Postgres de uma instância dentro do +// namespace, sem nada no nível do cluster. A senha é gerada e fica só no +// estado do Pulumi e no Secret. +func declareNamespacePostgres(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) (pulumi.Resource, error) { + password, err := random.NewRandomPassword(ctx, names.database+"-password", &random.RandomPasswordArgs{ + Length: pulumi.Int(passwordLength), Special: pulumi.Bool(false), + }, parent) + if err != nil { + return nil, err + } + if err := declarePostgresSecret(ctx, names, args, password.Result, parent); err != nil { + return nil, err + } + labels := pulumi.StringMap{"app.kubernetes.io/name": pulumi.String(names.database)} + if err := declarePostgresService(ctx, names, args, labels, parent); err != nil { + return nil, err + } + return appsv1.NewStatefulSet(ctx, names.database, postgresStatefulSetArgs(names, args, labels), parent) +} + +func declarePostgresSecret(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, password pulumi.StringOutput, parent pulumi.ResourceOption) error { + uri := pulumi.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", + postgresUser, password, names.database, postgresPort, postgresUser) + _, err := corev1.NewSecret(ctx, names.dbSecret, &corev1.SecretArgs{ + Metadata: namespaced(names.dbSecret, args.Namespace), + StringData: pulumi.StringMap{dbPasswordKey: password, dbSecretKey: uri}, + }, parent) + return err +} + +// declarePostgresService é headless: o StatefulSet exige um, e a API acha o +// banco pelo nome dele. +func declarePostgresService(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, labels pulumi.StringMap, parent pulumi.ResourceOption) error { + _, err := corev1.NewService(ctx, names.database, &corev1.ServiceArgs{ + Metadata: namespaced(names.database, args.Namespace), + Spec: corev1.ServiceSpecArgs{ + ClusterIP: pulumi.String("None"), + Selector: labels, + Ports: corev1.ServicePortArray{corev1.ServicePortArgs{Port: pulumi.Int(postgresPort)}}, + }, + }, parent) + return err +} + +func postgresStatefulSetArgs(names resourceNames, args MarkuppArgs, labels pulumi.StringMap) *appsv1.StatefulSetArgs { + return &appsv1.StatefulSetArgs{ + Metadata: namespaced(names.database, args.Namespace), + Spec: appsv1.StatefulSetSpecArgs{ + ServiceName: pulumi.String(names.database), + Replicas: pulumi.Int(1), + Selector: metav1.LabelSelectorArgs{MatchLabels: labels}, + Template: corev1.PodTemplateSpecArgs{ + Metadata: metav1.ObjectMetaArgs{Labels: labels}, + Spec: corev1.PodSpecArgs{Containers: corev1.ContainerArray{postgresContainer(names)}}, + }, + VolumeClaimTemplates: corev1.PersistentVolumeClaimTypeArray{postgresVolumeClaim(args)}, + }, + } +} + +// postgresContainer aponta PGDATA para um subdiretório porque a raiz de um +// volume ext4 tem lost+found, e o initdb recusa diretório não vazio. +func postgresContainer(names resourceNames) corev1.ContainerArgs { + return corev1.ContainerArgs{ + Name: pulumi.String("postgres"), + Image: pulumi.String(postgresImage), + Env: corev1.EnvVarArray{ + corev1.EnvVarArgs{Name: pulumi.String("POSTGRES_USER"), Value: pulumi.String(postgresUser)}, + corev1.EnvVarArgs{Name: pulumi.String("POSTGRES_DB"), Value: pulumi.String(postgresUser)}, + corev1.EnvVarArgs{Name: pulumi.String("PGDATA"), Value: pulumi.String(postgresDataPath + "/pgdata")}, + secretEnv("POSTGRES_PASSWORD", names.dbSecret, dbPasswordKey), + }, + Ports: corev1.ContainerPortArray{corev1.ContainerPortArgs{ContainerPort: pulumi.Int(postgresPort)}}, + VolumeMounts: corev1.VolumeMountArray{corev1.VolumeMountArgs{Name: pulumi.String("data"), MountPath: pulumi.String(postgresDataPath)}}, + ReadinessProbe: corev1.ProbeArgs{Exec: corev1.ExecActionArgs{Command: pulumi.ToStringArray([]string{"pg_isready", "-U", postgresUser})}}, + } +} + +func postgresVolumeClaim(args MarkuppArgs) corev1.PersistentVolumeClaimTypeArgs { + spec := corev1.PersistentVolumeClaimSpecArgs{ + AccessModes: pulumi.ToStringArray([]string{"ReadWriteOnce"}), + Resources: corev1.VolumeResourceRequirementsArgs{ + Requests: pulumi.StringMap{"storage": pulumi.String(args.Database.Size)}, + }, + } + if args.Database.StorageClass != "" { + spec.StorageClassName = pulumi.String(args.Database.StorageClass) + } + return corev1.PersistentVolumeClaimTypeArgs{ + Metadata: metav1.ObjectMetaArgs{Name: pulumi.String("data")}, + Spec: spec, + } +} + +func databaseURLEnv(names resourceNames) corev1.EnvVarArray { + return corev1.EnvVarArray{secretEnv("MARKUPP_DATABASE_URL", names.dbSecret, dbSecretKey)} +} + +func secretEnv(name, secret, key string) corev1.EnvVarArgs { + return corev1.EnvVarArgs{ + Name: pulumi.String(name), + ValueFrom: corev1.EnvVarSourceArgs{SecretKeyRef: corev1.SecretKeySelectorArgs{ + Name: pulumi.String(secret), + Key: pulumi.String(key), + }}, + } +} + +func namespaced(name, namespace string) metav1.ObjectMetaArgs { + return metav1.ObjectMetaArgs{Name: pulumi.String(name), Namespace: pulumi.String(namespace)} +} diff --git a/deploy/pulumi/go.mod b/deploy/pulumi/go.mod new file mode 100644 index 0000000..a3822bd --- /dev/null +++ b/deploy/pulumi/go.mod @@ -0,0 +1,132 @@ +module github.com/markupp-labs/markupp/deploy/pulumi + +go 1.26.6 + +require ( + github.com/pulumi/pulumi-go-provider v1.7.0 + github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.2 + github.com/pulumi/pulumi-random/sdk/v4 v4.21.2 + github.com/pulumi/pulumi/sdk/v3 v3.265.0 + github.com/stretchr/testify v1.12.1 +) + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.8 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/ebitengine/purego v0.10.2 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-alpha.2 // indirect + github.com/go-git/go-git/v6 v6.0.0-alpha.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect + github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect + github.com/hashicorp/hcl/v2 v2.24.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/compress v1.18.7 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.30 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/natefinch/atomic v1.0.1 // indirect + github.com/opentracing/basictracer-go v1.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pgavlin/fx v0.1.6 // indirect + github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/term v1.1.0 // indirect + github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect + github.com/pulumi/pulumi-cloud-sdk/go v1.20260924.0 // indirect + github.com/pulumi/pulumi/pkg/v3 v3.265.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.3.5 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xo/terminfo v1.2.0 // indirect + github.com/zalando/go-keyring v0.2.8 // indirect + github.com/zclconf/go-cty v1.16.3 // indirect + go.mongodb.org/mongo-driver v1.17.10 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/collector/featuregate v1.67.0 // indirect + go.opentelemetry.io/collector/pdata v1.67.0 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/bridge/opentracing v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect + go.opentelemetry.io/otel/log v0.22.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/sdk v1.46.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.22.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.uber.org/atomic v1.12.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/tools v0.50.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 // indirect + google.golang.org/grpc v1.84.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.5.1 // indirect +) diff --git a/deploy/pulumi/go.sum b/deploy/pulumi/go.sum new file mode 100644 index 0000000..4e16bc9 --- /dev/null +++ b/deploy/pulumi/go.sum @@ -0,0 +1,364 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= +github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= +github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= +github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= +github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= +github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v6 v6.0.0-alpha.2 h1:1Sv5WemXL8CxKrAx1gioJ+uHNb2bZJhiQLfwSZ4Et8c= +github.com/go-git/go-billy/v6 v6.0.0-alpha.2/go.mod h1:r/bsv9i/iDyyEU8/Z6mjC+YraOVwie1ddfUqBCElKXQ= +github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA= +github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ= +github.com/go-git/go-git/v6 v6.0.0-alpha.5 h1:sE+OlkHgYWNMVmN1s9sR7uyFgsWLtxcNWse/vBYKxRE= +github.com/go-git/go-git/v6 v6.0.0-alpha.5/go.mod h1:3IjhiZnM+uBmUrOGSeqrJpsmi4Vd0H2NZO/uK2a7d0s= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= +github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= +github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= +github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/opentracing-contrib/go-grpc v0.1.4 h1:m9VukGYQ+tfcgtwdsifjjFtAoWRqmcbnGhgRQcZ+ZQA= +github.com/opentracing-contrib/go-grpc v0.1.4/go.mod h1:xP8rkWX/qdQJVTkf01D6Fc002zN0WUMKsvuKg7Tf93E= +github.com/opentracing-contrib/go-grpc/test v0.0.0-20260727233914-064b4d2bcbbf h1:xfFA+XblffDh3LsTrbSG1MHpmrXsY/9GQI4sbCnY+kI= +github.com/opentracing-contrib/go-grpc/test v0.0.0-20260727233914-064b4d2bcbbf/go.mod h1:AYO7D5MbvWwD7/UggkDbqyW0pDt7KZyQkHCn4IC2i/w= +github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= +github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= +github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= +github.com/pgavlin/fx/v2 v2.0.12 h1:SjjaJ68Dt8Z4zHwOpY/RPijd7lShs6xYupJbF9ra00M= +github.com/pgavlin/fx/v2 v2.0.12/go.mod h1:M/nF/ooAOy+NUBooYYXl2REARzJ/giPJxfMs8fINfKc= +github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 h1:LoCV5cscNVWyK5ChN/uCoIFJz8jZD63VQiGJIRgr6uo= +github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386/go.mod h1:MRxHTJrf9FhdfNQ8Hdeh9gmHevC9RJE/fu8M3JIGjoE= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= +github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= +github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= +github.com/pulumi/pulumi-cloud-sdk/go v1.20260924.0 h1:NCX/ta8vQNIp3E1k1SbRtRO6FdCVSW6vHIyoGfby10k= +github.com/pulumi/pulumi-cloud-sdk/go v1.20260924.0/go.mod h1:NPA79X6Wmyr5zNKd+N+RiVJN8duwIrRmIaed8HCL9EM= +github.com/pulumi/pulumi-go-provider v1.7.0 h1:wrkDxqcHQTO3CjAjDqjBfYxQCMUNaYLbK/bEl8AbvMo= +github.com/pulumi/pulumi-go-provider v1.7.0/go.mod h1:FIiR3Z9U1Qqk/qAGkctluJDFqMK9NWhj9qDJ9+AIVzs= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.2 h1:W9XLxp6EzQGf14DumMCGOnT9QcRRSgsI+f5Do3pMqqU= +github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.34.2/go.mod h1:/klrd3ib4AMRlFMjrxkv5r76x3Bdr68wIMEKfRkRzHU= +github.com/pulumi/pulumi-random/sdk/v4 v4.21.2 h1:QZFU//Q+PRCCjFsKDpmMu+4XLOBXjjOuuoqDuw5TGmU= +github.com/pulumi/pulumi-random/sdk/v4 v4.21.2/go.mod h1:G9IK4IFL/A2F9fgDMLWRpaJJHyapYvVmB6eJm9cjg48= +github.com/pulumi/pulumi/pkg/v3 v3.265.0 h1:g8Hc0J1/MQIKNjBM+eIZWMpxNbJEnOESfnRA+VWsgxs= +github.com/pulumi/pulumi/pkg/v3 v3.265.0/go.mod h1:8fy15Mix7X+To9HGCs/PbM5ao92y59+Yra9m5ne0SsY= +github.com/pulumi/pulumi/sdk/v3 v3.265.0 h1:SGeFgCAM4YYGFfzqtm3xFXewZg6iIxzr8QWr1XzxK8c= +github.com/pulumi/pulumi/sdk/v3 v3.265.0/go.mod h1:UFc/1vJjWOHd8C7eZw1QpyWgtoImQw2L9wDpQYsdoNI= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.3.5 h1:UZEiaZ55nlXGDL92scoVuw00RmiRCazIEmvPSbSvt8Y= +github.com/segmentio/encoding v0.3.5/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4= +github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.mongodb.org/mongo-driver v1.17.10 h1:kdAgQvu8TROXZpSkJQd5wzfaNCCrMbpZyKFtQ6qkPCE= +go.mongodb.org/mongo-driver v1.17.10/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/collector/featuregate v1.67.0 h1:x6aIJtcU7hp37lLutdbUyi2x7CAcHsK/QXMLEpd8w1I= +go.opentelemetry.io/collector/featuregate v1.67.0/go.mod h1:dRYifiJa2vQ6LWpPwHny4mL82mnGWsWEVeVWw+DhYJw= +go.opentelemetry.io/collector/internal/testutil v0.161.0 h1:eNQHH/z5E6M15aQ409cF9j1Q49GOIJDhvcgNXSnxRZk= +go.opentelemetry.io/collector/internal/testutil v0.161.0/go.mod h1:FV43FoAsh4fP615Sc5ZSh7iPMgZaSgha6ngavix9OEI= +go.opentelemetry.io/collector/pdata v1.67.0 h1:dL654J7PNHc6EGHRa7j6gYtbSDBC7zs8Xl6Dcn4XH2E= +go.opentelemetry.io/collector/pdata v1.67.0/go.mod h1:2CSCUtdgTXIGIk7GXoYuBZG2VjkrTzqcGIh6ukgXLwc= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.1 h1:5sHc4ToTFjfSZCtGAAM6jPunICAmJX73htv372T4ipc= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.1/go.mod h1:oa6kgvyz/3GYW04dohd0++xJIH4xdQY8PAbpeCMaM8M= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/bridge/opentracing v1.45.0 h1:hFf959SYHZpFc8ZWlqiYlnH3Ke5w4fcWZh7He+CmqXk= +go.opentelemetry.io/otel/bridge/opentracing v1.45.0/go.mod h1:etOoQoentzhTX0XTaRJmwlsiQx8TXD7mHo76YxNeBYg= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 h1:Bu39F5tzJct+f2IZbB8989fwyTps3c8e7EsUQsz+vs8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0/go.mod h1:dJUwod88EsFgYCqrDHaSPzhiY9pBUpt0d85/qSfua7k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY= +go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0= +go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk= +go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g= +go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg= +go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +go.opentelemetry.io/proto/slim/otlp v1.11.0 h1:zB37f+f99+y6UIZR4h7UpwbXd5kFNyip35U7GaJ/Jik= +go.opentelemetry.io/proto/slim/otlp v1.11.0/go.mod h1:mI3DeND+VXZuA4keqFPKDJ3BklwveYm1JqBcEWKDEOM= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.4.0 h1:mt+DWtks0biKnz0jXMpDbxWN0CHJi6OJDKe4GcREkcs= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.4.0/go.mod h1:7UXaX/7uT+kumUHd3LIWyjMlklEp0mPlrE9xmtbG6/8= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.4.0 h1:rLHkdB6eHDiRSIoz0cvNuTJsVJBxaL6IyS1e9BSaXLY= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.4.0/go.mod h1:BrX0dmOGsMuWNXXbFafTD7Gb6F3yK+2czVQ6+c24Cnk= +go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= +go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459 h1:GS9OIt/j7c8bvBjYNgnKQysVfmV7e4jM0H8ZK95G4t8= +google.golang.org/genproto/googleapis/api v0.0.0-20260921155816-b14227669459/go.mod h1:PX5/4vemwVoXtwEcRDWwcR1/r0qrosfx3qoVADMwnVE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 h1:b0xCahf3FK2m2Cv0p4vTozGPWncCvLfwV86UNg8xWU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= +lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= +pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= +pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk= diff --git a/deploy/pulumi/ingress.go b/deploy/pulumi/ingress.go new file mode 100644 index 0000000..5a690a7 --- /dev/null +++ b/deploy/pulumi/ingress.go @@ -0,0 +1,113 @@ +package main + +import ( + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes" + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +const ( + httpListener = "http" + httpsListener = "https" +) + +// declareIngress cria Issuer, Gateway e HTTPRoute no namespace do markupp, ou +// só o HTTPRoute quando há um Gateway existente. O Issuer é do namespace, e +// não ClusterIssuer, para a credencial do CI não precisar de permissão no +// cluster inteiro. +func declareIngress(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { + if args.ExistingGateway != nil { + return declareRoute(ctx, names, args, existingGatewayRef(*args.ExistingGateway), parent) + } + if err := declareIssuer(ctx, names, args, parent); err != nil { + return err + } + if err := declareGateway(ctx, names, args, parent); err != nil { + return err + } + return declareRoute(ctx, names, args, gatewayRef(names, args, httpsListener), parent) +} + +func declareIssuer(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { + spec := pulumi.Map{"selfSigned": pulumi.Map{}} + if args.CertificateIssuer == issuerACME { + spec = acmeIssuerSpec(names, args) + } + return declareCustom(ctx, "cert-manager.io/v1", "Issuer", names.issuer, args, spec, parent) +} + +// acmeIssuerSpec resolve o desafio HTTP-01 pelo listener http do próprio +// Gateway, como o cert-manager faz com Gateway API. +func acmeIssuerSpec(names resourceNames, args MarkuppArgs) pulumi.Map { + solver := pulumi.Map{"http01": pulumi.Map{"gatewayHTTPRoute": pulumi.Map{ + "parentRefs": pulumi.Array{gatewayRef(names, args, httpListener)}, + }}} + return pulumi.Map{"acme": pulumi.Map{ + "server": pulumi.String("https://acme-v02.api.letsencrypt.org/directory"), + "email": pulumi.String(args.AcmeEmail), + "privateKeySecretRef": pulumi.Map{"name": pulumi.String(names.issuer + "-account")}, + "solvers": pulumi.Array{solver}, + }} +} + +func declareGateway(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { + https := pulumi.Map{ + "name": pulumi.String(httpsListener), "protocol": pulumi.String("HTTPS"), "port": pulumi.Int(443), + "hostname": pulumi.String(args.Host), + "tls": pulumi.Map{ + "mode": pulumi.String("Terminate"), + "certificateRefs": pulumi.Array{pulumi.Map{"name": pulumi.String(names.tlsSecret)}}, + }, + } + http := pulumi.Map{"name": pulumi.String(httpListener), "protocol": pulumi.String("HTTP"), "port": pulumi.Int(80)} + gateway := &apiextensions.CustomResourceArgs{ + ApiVersion: pulumi.String("gateway.networking.k8s.io/v1"), + Kind: pulumi.String("Gateway"), + Metadata: metav1.ObjectMetaArgs{ + Name: pulumi.String(names.gateway), Namespace: pulumi.String(args.Namespace), + Annotations: pulumi.StringMap{"cert-manager.io/issuer": pulumi.String(names.issuer)}, + }, + OtherFields: kubernetes.UntypedArgs{"spec": pulumi.Map{ + "gatewayClassName": pulumi.String(args.GatewayClassName), + "listeners": pulumi.Array{http, https}, + }}, + } + _, err := apiextensions.NewCustomResource(ctx, names.gateway, gateway, parent) + return err +} + +func declareRoute(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, gateway pulumi.Map, parent pulumi.ResourceOption) error { + backend := pulumi.Map{"name": pulumi.String(names.api), "port": pulumi.Int(apiPort)} + return declareCustom(ctx, "gateway.networking.k8s.io/v1", "HTTPRoute", names.api, args, pulumi.Map{ + "parentRefs": pulumi.Array{gateway}, + "hostnames": pulumi.StringArray{pulumi.String(args.Host)}, + "rules": pulumi.Array{pulumi.Map{"backendRefs": pulumi.Array{backend}}}, + }, parent) +} + +func gatewayRef(names resourceNames, args MarkuppArgs, listener string) pulumi.Map { + return pulumi.Map{ + "name": pulumi.String(names.gateway), + "namespace": pulumi.String(args.Namespace), + "sectionName": pulumi.String(listener), + } +} + +func existingGatewayRef(ref GatewayRefArgs) pulumi.Map { + gateway := pulumi.Map{"name": pulumi.String(ref.Name), "namespace": pulumi.String(ref.Namespace)} + if ref.SectionName != "" { + gateway["sectionName"] = pulumi.String(ref.SectionName) + } + return gateway +} + +func declareCustom(ctx *pulumi.Context, apiVersion, kind, name string, args MarkuppArgs, spec pulumi.Map, parent pulumi.ResourceOption) error { + _, err := apiextensions.NewCustomResource(ctx, name, &apiextensions.CustomResourceArgs{ + ApiVersion: pulumi.String(apiVersion), + Kind: pulumi.String(kind), + Metadata: namespaced(name, args.Namespace), + OtherFields: kubernetes.UntypedArgs{"spec": spec}, + }, parent) + return err +} diff --git a/deploy/pulumi/main.go b/deploy/pulumi/main.go new file mode 100644 index 0000000..0228c0c --- /dev/null +++ b/deploy/pulumi/main.go @@ -0,0 +1,27 @@ +// Command pulumi-resource-markupp expõe o componente Markupp como pacote +// Pulumi, consumido com `pulumi package add` a partir deste diretório. +package main + +import ( + "context" + "fmt" + "os" + + "github.com/pulumi/pulumi-go-provider/infer" +) + +// version acompanha a tag do repositório. O release sobrescreve com -ldflags. +var version = "0.0.0-dev" + +func main() { + provider, err := infer.NewProviderBuilder(). + WithComponents(infer.ComponentF(NewMarkupp)). + Build() + if err == nil { + err = provider.Run(context.Background(), "markupp", version) + } + if err != nil { + fmt.Fprintf(os.Stderr, "provider markupp encerrado: %v\n", err) + os.Exit(1) + } +} diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go new file mode 100644 index 0000000..fdcacfa --- /dev/null +++ b/deploy/pulumi/markupp.go @@ -0,0 +1,173 @@ +package main + +import ( + "fmt" + + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +const ( + componentType = "markupp:index:Markupp" + defaultGatewayClass = "cilium" + issuerACME = "acme" + issuerSelfSigned = "selfSigned" + defaultAPIReplicas = 2 + defaultDBSize = "10Gi" + apiPort = 8080 +) + +// Markupp é a instalação do markupp num namespace do Kubernetes: banco, job de +// migração, API e entrada com TLS. +type Markupp struct { + pulumi.ResourceState + + URL pulumi.StringOutput `pulumi:"url"` +} + +// MarkuppArgs são os parâmetros da instalação. +type MarkuppArgs struct { + // Namespace já existente onde tudo é criado. Criar o namespace fica com + // quem administra o cluster. + Namespace string `pulumi:"namespace"` + // Image é a imagem do servidor, com tag de versão. + Image string `pulumi:"image"` + // Host é o domínio público da API. + Host string `pulumi:"host"` + // CertificateIssuer é acme, com Let's Encrypt, ou selfSigned, para + // cluster sem domínio público. Padrão acme. + CertificateIssuer string `pulumi:"certificateIssuer,optional"` + // AcmeEmail recebe os avisos do Let's Encrypt. Obrigatório com acme. + AcmeEmail string `pulumi:"acmeEmail,optional"` + // GatewayClassName é a classe do Gateway. Padrão cilium. + GatewayClassName string `pulumi:"gatewayClassName,optional"` + // ExistingGateway pendura o HTTPRoute num Gateway que já existe no + // cluster, sem criar Gateway nem Issuer. O certificado fica com quem + // administra esse Gateway. + ExistingGateway *GatewayRefArgs `pulumi:"existingGateway,optional"` + // APIReplicas é o número de réplicas da API. Padrão 2, e 0 desliga a API + // sem apagar nada. + APIReplicas *int `pulumi:"apiReplicas,optional"` + // Database define de onde vem o PostgreSQL. + Database DatabaseArgs `pulumi:"database"` +} + +// GatewayRefArgs aponta para um Gateway de outro namespace. Ele precisa +// aceitar rotas do namespace do markupp em allowedRoutes. +type GatewayRefArgs struct { + Name string `pulumi:"name"` + Namespace string `pulumi:"namespace"` + // SectionName é o listener do Gateway. Vazio deixa o Gateway escolher + // pelo host. + SectionName string `pulumi:"sectionName,optional"` +} + +// DatabaseArgs escolhe entre um PostgreSQL externo e um PostgreSQL de uma +// instância dentro do namespace. +type DatabaseArgs struct { + // ExternalURL aponta para um PostgreSQL já existente. Vazio sobe um + // PostgreSQL no namespace, sem nada no nível do cluster. + ExternalURL pulumi.StringInput `pulumi:"externalUrl,optional" provider:"secret"` + // StorageClass do volume do PostgreSQL do namespace. + StorageClass string `pulumi:"storageClass,optional"` + // Size do volume do PostgreSQL do namespace. Padrão 10Gi. + Size string `pulumi:"size,optional"` +} + +// NewMarkupp declara a instalação completa sob o componente name. +func NewMarkupp(ctx *pulumi.Context, name string, args MarkuppArgs, opts ...pulumi.ResourceOption) (*Markupp, error) { + if err := validate(args); err != nil { + return nil, err + } + args = withDefaults(args) + self := &Markupp{} + if err := ctx.RegisterComponentResource(componentType, name, self, opts...); err != nil { + return nil, err + } + if err := declareResources(ctx, name, args, pulumi.Parent(self)); err != nil { + return nil, err + } + self.URL = pulumi.Sprintf("https://%s", args.Host) + return self, ctx.RegisterResourceOutputs(self, pulumi.Map{"url": self.URL}) +} + +func declareResources(ctx *pulumi.Context, name string, args MarkuppArgs, parent pulumi.ResourceOption) error { + names := newResourceNames(name) + db, err := declareDatabase(ctx, names, args, parent) + if err != nil { + return err + } + migration, err := declareMigrationJob(ctx, names, args, db, parent) + if err != nil { + return err + } + if err := declareAPI(ctx, names, args, parent, pulumi.DependsOn([]pulumi.Resource{migration})); err != nil { + return err + } + return declareIngress(ctx, names, args, parent) +} + +type requiredField struct{ field, value string } + +func validate(args MarkuppArgs) error { + required := []requiredField{{"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}} + ingress, err := ingressRequiredFields(args) + if err != nil { + return err + } + for _, r := range append(required, ingress...) { + if r.value == "" { + return fmt.Errorf("campo %s vazio, esperado texto não vazio (namespace=%q image=%q host=%q certificateIssuer=%q acmeEmail=%q)", + r.field, args.Namespace, args.Image, args.Host, args.CertificateIssuer, args.AcmeEmail) + } + } + return nil +} + +// ingressRequiredFields lista o que a entrada exige: com Gateway existente, a +// referência a ele; sem, o email do ACME quando o emissor é acme. +func ingressRequiredFields(args MarkuppArgs) ([]requiredField, error) { + if ref := args.ExistingGateway; ref != nil { + return []requiredField{{"existingGateway.name", ref.Name}, {"existingGateway.namespace", ref.Namespace}}, nil + } + switch args.CertificateIssuer { + case "", issuerACME: + return []requiredField{{"acmeEmail", args.AcmeEmail}}, nil + case issuerSelfSigned: + return nil, nil + } + return nil, fmt.Errorf("certificateIssuer=%q, esperado %q ou %q", args.CertificateIssuer, issuerACME, issuerSelfSigned) +} + +func withDefaults(args MarkuppArgs) MarkuppArgs { + if args.CertificateIssuer == "" { + args.CertificateIssuer = issuerACME + } + if args.GatewayClassName == "" { + args.GatewayClassName = defaultGatewayClass + } + if args.APIReplicas == nil { + replicas := defaultAPIReplicas + args.APIReplicas = &replicas + } + if args.Database.Size == "" { + args.Database.Size = defaultDBSize + } + return args +} + +// resourceNames centraliza os nomes que um recurso usa para achar outro. +type resourceNames struct { + component, database, dbSecret, api, gateway, issuer, tlsSecret string +} + +func newResourceNames(component string) resourceNames { + return resourceNames{ + component: component, + database: component + "-db", + dbSecret: component + "-db-app", + api: component + "-api", + gateway: component + "-gateway", + issuer: component + "-issuer", + tlsSecret: component + "-tls", + } +} diff --git a/deploy/pulumi/markupp_test.go b/deploy/pulumi/markupp_test.go new file mode 100644 index 0000000..fccf290 --- /dev/null +++ b/deploy/pulumi/markupp_test.go @@ -0,0 +1,314 @@ +package main + +import ( + "sync" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + randomPasswordType = "random:index/randomPassword:RandomPassword" + senhaDeTeste = "senha-gerada" +) + +// fakeResourceMonitor faz o papel do motor do Pulumi: aceita cada recurso +// declarado e guarda os inputs para o teste inspecionar. Para a senha +// aleatória, devolve um valor fixo no lugar do que o provider geraria. +type fakeResourceMonitor struct { + mu sync.Mutex + resources []pulumi.MockResourceArgs +} + +func (f *fakeResourceMonitor) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.resources = append(f.resources, args) + state := args.Inputs.Copy() + if args.TypeToken == randomPasswordType { + state["result"] = resource.NewStringProperty(senhaDeTeste) + } + return args.Name + "-id", state, nil +} + +func (f *fakeResourceMonitor) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) { + return resource.PropertyMap{}, nil +} + +func (f *fakeResourceMonitor) byType(token string) []pulumi.MockResourceArgs { + f.mu.Lock() + defer f.mu.Unlock() + var found []pulumi.MockResourceArgs + for _, r := range f.resources { + if r.TypeToken == token { + found = append(found, r) + } + } + return found +} + +func (f *fakeResourceMonitor) single(t *testing.T, token string) resource.PropertyMap { + t.Helper() + found := f.byType(token) + require.Len(t, found, 1, "esperado um recurso %s", token) + return found[0].Inputs +} + +func argsDeTeste() MarkuppArgs { + return MarkuppArgs{ + Namespace: "markupp", + Image: "ghcr.io/markupp-labs/markupp:v1.1.0", + Host: "markupp.dev.br", + AcmeEmail: "equipe@markupp.dev.br", + Database: DatabaseArgs{StorageClass: "longhorn-fast"}, + } +} + +func deploy(t *testing.T, args MarkuppArgs) *fakeResourceMonitor { + t.Helper() + monitor := &fakeResourceMonitor{} + err := pulumi.RunErr(func(ctx *pulumi.Context) error { + _, err := NewMarkupp(ctx, "markupp", args) + return err + }, pulumi.WithMocks("markupp", "teste", monitor)) + require.NoError(t, err) + return monitor +} + +func declareErr(args MarkuppArgs) error { + return pulumi.RunErr(func(ctx *pulumi.Context) error { + _, err := NewMarkupp(ctx, "markupp", args) + return err + }, pulumi.WithMocks("markupp", "teste", &fakeResourceMonitor{})) +} + +func at(t *testing.T, pm resource.PropertyMap, keys ...string) resource.PropertyValue { + t.Helper() + value := resource.NewObjectProperty(pm) + for _, key := range keys { + for value.IsSecret() { + value = value.SecretValue().Element + } + require.True(t, value.IsObject(), "esperado objeto antes de %q, recebido %v", key, value) + value = value.ObjectValue()[resource.PropertyKey(key)] + } + return value +} + +func plain(value resource.PropertyValue) resource.PropertyValue { + for value.IsSecret() { + value = value.SecretValue().Element + } + return value +} + +func firstContainer(t *testing.T, pm resource.PropertyMap) resource.PropertyMap { + t.Helper() + containers := at(t, pm, "spec", "template", "spec", "containers").ArrayValue() + require.NotEmpty(t, containers) + return containers[0].ObjectValue() +} + +func TestNewMarkupp_SemNamespace_RetornaErroComOCampo(t *testing.T) { + args := argsDeTeste() + args.Namespace = "" + + err := declareErr(args) + + require.Error(t, err) + assert.Contains(t, err.Error(), "namespace") +} + +func TestNewMarkupp_SemBancoExterno_CriaPostgresNoNamespaceComVolume(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + db := monitor.single(t, "kubernetes:apps/v1:StatefulSet") + assert.Equal(t, "markupp", at(t, db, "metadata", "namespace").StringValue()) + assert.Equal(t, 1.0, at(t, db, "spec", "replicas").NumberValue()) + assert.Equal(t, "postgres:17.9-alpine3.23", firstContainer(t, db)["image"].StringValue(), + "mesma versão fixa do compose e do testcontainers") + claim := at(t, db, "spec", "volumeClaimTemplates").ArrayValue()[0].ObjectValue() + assert.Equal(t, "longhorn-fast", at(t, claim, "spec", "storageClassName").StringValue()) + assert.Equal(t, "10Gi", at(t, claim, "spec", "resources", "requests", "storage").StringValue()) +} + +func TestNewMarkupp_SemBancoExterno_GuardaSenhaGeradaEURLNoSecret(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + require.Len(t, monitor.byType(randomPasswordType), 1) + secret := monitor.single(t, "kubernetes:core/v1:Secret") + assert.Equal(t, senhaDeTeste, plain(at(t, secret, "stringData", "password")).StringValue()) + assert.Equal(t, "postgres://markupp:"+senhaDeTeste+"@markupp-db:5432/markupp?sslmode=disable", + plain(at(t, secret, "stringData", "uri")).StringValue()) +} + +func TestNewMarkupp_SemBancoExterno_PostgresLeASenhaDoSecret(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + db := monitor.single(t, "kubernetes:apps/v1:StatefulSet") + var ref resource.PropertyValue + for _, env := range firstContainer(t, db)["env"].ArrayValue() { + if env.ObjectValue()["name"].StringValue() == "POSTGRES_PASSWORD" { + ref = at(t, env.ObjectValue(), "valueFrom", "secretKeyRef") + } + } + require.True(t, ref.IsObject(), "esperado POSTGRES_PASSWORD vindo de Secret") + assert.Equal(t, "markupp-db-app", ref.ObjectValue()["name"].StringValue()) + assert.Equal(t, "password", ref.ObjectValue()["key"].StringValue()) +} + +func TestNewMarkupp_ComBancoExterno_NaoCriaPostgresEGuardaURLEmSecret(t *testing.T) { + args := argsDeTeste() + args.Database.ExternalURL = pulumi.String("postgres://markupp@db.externo:5432/markupp") + + monitor := deploy(t, args) + + assert.Empty(t, monitor.byType("kubernetes:apps/v1:StatefulSet")) + assert.Empty(t, monitor.byType(randomPasswordType)) + secret := monitor.single(t, "kubernetes:core/v1:Secret") + assert.Equal(t, "postgres://markupp@db.externo:5432/markupp", + at(t, secret, "stringData", "uri").StringValue()) +} + +func TestNewMarkupp_JobDeMigracao_RodaOSubcomandoMigrateNaImagem(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + job := monitor.single(t, "kubernetes:batch/v1:Job") + container := firstContainer(t, job) + assert.Equal(t, "ghcr.io/markupp-labs/markupp:v1.1.0", container["image"].StringValue()) + assert.Equal(t, "migrate", container["args"].ArrayValue()[0].StringValue()) +} + +func TestNewMarkupp_DeploymentDaAPI_LeOBancoDoSecret(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + api := monitor.single(t, "kubernetes:apps/v1:Deployment") + container := firstContainer(t, api) + env := container["env"].ArrayValue()[0].ObjectValue() + assert.Equal(t, "MARKUPP_DATABASE_URL", env["name"].StringValue()) + ref := at(t, env, "valueFrom", "secretKeyRef") + assert.Equal(t, "markupp-db-app", at(t, ref.ObjectValue(), "name").StringValue()) + assert.Equal(t, "uri", at(t, ref.ObjectValue(), "key").StringValue()) +} + +func TestNewMarkupp_DeploymentDaAPI_UsaDuasReplicasESondaHealthz(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + api := monitor.single(t, "kubernetes:apps/v1:Deployment") + assert.Equal(t, 2.0, at(t, api, "spec", "replicas").NumberValue()) + container := firstContainer(t, api) + assert.Equal(t, "/healthz", at(t, container, "readinessProbe", "httpGet", "path").StringValue()) +} + +func TestNewMarkupp_Entrada_RoteiaOHostPeloGatewayComTLS(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + route := monitor.single(t, "kubernetes:gateway.networking.k8s.io/v1:HTTPRoute") + assert.Equal(t, "markupp.dev.br", at(t, route, "spec", "hostnames").ArrayValue()[0].StringValue()) + gateway := monitor.single(t, "kubernetes:gateway.networking.k8s.io/v1:Gateway") + assert.Equal(t, "cilium", at(t, gateway, "spec", "gatewayClassName").StringValue()) + listeners := at(t, gateway, "spec", "listeners").ArrayValue() + require.Len(t, listeners, 2, "esperado um listener http para o desafio ACME e um https") + assert.Equal(t, "markupp-issuer", + at(t, gateway, "metadata", "annotations").ObjectValue()["cert-manager.io/issuer"].StringValue()) +} + +func TestNewMarkupp_Issuer_UsaOEmailACMEInformado(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + issuer := monitor.single(t, "kubernetes:cert-manager.io/v1:Issuer") + assert.Equal(t, "equipe@markupp.dev.br", at(t, issuer, "spec", "acme", "email").StringValue()) +} + +func TestNewMarkupp_EmissorAutoassinado_DispensaEmailEACME(t *testing.T) { + args := argsDeTeste() + args.CertificateIssuer = "selfSigned" + args.AcmeEmail = "" + + monitor := deploy(t, args) + + issuer := monitor.single(t, "kubernetes:cert-manager.io/v1:Issuer") + assert.True(t, at(t, issuer, "spec", "selfSigned").IsObject(), "esperado spec.selfSigned") + assert.True(t, at(t, issuer, "spec", "acme").IsNull(), "emissor autoassinado nao fala com ACME") +} + +func TestNewMarkupp_EmissorACMESemEmail_RetornaErroComOCampo(t *testing.T) { + args := argsDeTeste() + args.AcmeEmail = "" + + err := declareErr(args) + + require.Error(t, err) + assert.Contains(t, err.Error(), "acmeEmail") + assert.Contains(t, err.Error(), "certificateIssuer") +} + +func TestNewMarkupp_EmissorDesconhecido_RetornaErroComOValor(t *testing.T) { + args := argsDeTeste() + args.CertificateIssuer = "letsencrypt" + + err := declareErr(args) + + require.Error(t, err) + assert.Contains(t, err.Error(), "letsencrypt") + assert.Contains(t, err.Error(), "selfSigned") +} + +func TestNewMarkupp_ZeroReplicas_DeixaAAPISemPods(t *testing.T) { + args := argsDeTeste() + zero := 0 + args.APIReplicas = &zero + + monitor := deploy(t, args) + + api := monitor.single(t, "kubernetes:apps/v1:Deployment") + assert.Equal(t, 0.0, at(t, api, "spec", "replicas").NumberValue(), "zero réplicas pedido não vira o padrão") +} + +func gatewayDoCluster() *GatewayRefArgs { + return &GatewayRefArgs{Name: "gateway-compartilhado", Namespace: "gateway", SectionName: "https-markupp"} +} + +func TestNewMarkupp_GatewayExistente_NaoCriaGatewayNemIssuer(t *testing.T) { + args := argsDeTeste() + args.ExistingGateway = gatewayDoCluster() + + monitor := deploy(t, args) + + assert.Empty(t, monitor.byType("kubernetes:gateway.networking.k8s.io/v1:Gateway")) + assert.Empty(t, monitor.byType("kubernetes:cert-manager.io/v1:Issuer")) +} + +func TestNewMarkupp_GatewayExistente_PenduraOHTTPRouteNele(t *testing.T) { + args := argsDeTeste() + args.ExistingGateway = gatewayDoCluster() + + monitor := deploy(t, args) + + route := monitor.single(t, "kubernetes:gateway.networking.k8s.io/v1:HTTPRoute") + parent := at(t, route, "spec", "parentRefs").ArrayValue()[0].ObjectValue() + assert.Equal(t, "gateway-compartilhado", parent["name"].StringValue()) + assert.Equal(t, "gateway", parent["namespace"].StringValue()) + assert.Equal(t, "https-markupp", parent["sectionName"].StringValue()) +} + +func TestNewMarkupp_GatewayExistente_DispensaEmailACME(t *testing.T) { + args := argsDeTeste() + args.ExistingGateway = gatewayDoCluster() + args.AcmeEmail = "" + + assert.NoError(t, declareErr(args), "o certificado é de quem administra o Gateway existente") +} + +func TestNewMarkupp_GatewayExistenteSemNome_RetornaErroComOCampo(t *testing.T) { + args := argsDeTeste() + args.ExistingGateway = &GatewayRefArgs{Namespace: "gateway"} + + err := declareErr(args) + + require.Error(t, err) + assert.Contains(t, err.Error(), "existingGateway.name") +} diff --git a/deploy/pulumi/workload.go b/deploy/pulumi/workload.go new file mode 100644 index 0000000..dc57ac0 --- /dev/null +++ b/deploy/pulumi/workload.go @@ -0,0 +1,92 @@ +package main + +import ( + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" + batchv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/batch/v1" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +// migrationBackoff cobre o tempo de o PostgreSQL aceitar conexão na primeira +// instalação: o Job falha rápido e tenta de novo até o banco responder. +const migrationBackoff = 10 + +// imageUID é o uid do usuário markupp criado no Dockerfile do servidor. +const imageUID = 10001 + +// declareMigrationJob aplica o schema antes da API. Sem nome fixo, o Pulumi +// gera um nome por versão e troca o Job quando a imagem muda. +func declareMigrationJob(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, db pulumi.Resource, parent pulumi.ResourceOption) (pulumi.Resource, error) { + container := serverContainer(args, names) + container.Args = pulumi.StringArray{pulumi.String("migrate")} + return batchv1.NewJob(ctx, names.component+"-migrate", &batchv1.JobArgs{ + Metadata: metav1.ObjectMetaArgs{Namespace: pulumi.String(args.Namespace)}, + Spec: batchv1.JobSpecArgs{ + BackoffLimit: pulumi.Int(migrationBackoff), + Template: corev1.PodTemplateSpecArgs{Spec: corev1.PodSpecArgs{ + RestartPolicy: pulumi.String("Never"), + SecurityContext: nonRootPod(), + Containers: corev1.ContainerArray{container}, + }}, + }, + }, parent, pulumi.DependsOn([]pulumi.Resource{db})) +} + +func declareAPI(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, opts ...pulumi.ResourceOption) error { + labels := pulumi.StringMap{"app.kubernetes.io/name": pulumi.String(names.api)} + if _, err := appsv1.NewDeployment(ctx, names.api, apiDeploymentArgs(names, args, labels), opts...); err != nil { + return err + } + _, err := corev1.NewService(ctx, names.api, &corev1.ServiceArgs{ + Metadata: namespaced(names.api, args.Namespace), + Spec: corev1.ServiceSpecArgs{ + Selector: labels, + Ports: corev1.ServicePortArray{corev1.ServicePortArgs{Port: pulumi.Int(apiPort)}}, + }, + }, opts...) + return err +} + +func apiDeploymentArgs(names resourceNames, args MarkuppArgs, labels pulumi.StringMap) *appsv1.DeploymentArgs { + container := serverContainer(args, names) + container.Ports = corev1.ContainerPortArray{corev1.ContainerPortArgs{ContainerPort: pulumi.Int(apiPort)}} + container.ReadinessProbe = healthzProbe() + container.LivenessProbe = healthzProbe() + return &appsv1.DeploymentArgs{ + Metadata: namespaced(names.api, args.Namespace), + Spec: appsv1.DeploymentSpecArgs{ + Replicas: pulumi.Int(*args.APIReplicas), + Selector: metav1.LabelSelectorArgs{MatchLabels: labels}, + Template: corev1.PodTemplateSpecArgs{ + Metadata: metav1.ObjectMetaArgs{Labels: labels}, + Spec: corev1.PodSpecArgs{SecurityContext: nonRootPod(), Containers: corev1.ContainerArray{container}}, + }, + }, + } +} + +func serverContainer(args MarkuppArgs, names resourceNames) corev1.ContainerArgs { + return corev1.ContainerArgs{ + Name: pulumi.String("markupp"), + Image: pulumi.String(args.Image), + Env: databaseURLEnv(names), + SecurityContext: corev1.SecurityContextArgs{ + AllowPrivilegeEscalation: pulumi.Bool(false), + ReadOnlyRootFilesystem: pulumi.Bool(true), + }, + } +} + +func healthzProbe() corev1.ProbeArgs { + return corev1.ProbeArgs{HttpGet: corev1.HTTPGetActionArgs{ + Path: pulumi.String("/healthz"), + Port: pulumi.Int(apiPort), + }} +} + +// nonRootPod usa o uid numérico do usuário da imagem: com nome de usuário o +// kubelet não consegue provar que o contêiner não roda como root. +func nonRootPod() corev1.PodSecurityContextArgs { + return corev1.PodSecurityContextArgs{RunAsNonRoot: pulumi.Bool(true), RunAsUser: pulumi.Int(imageUID)} +} diff --git a/deploy/stacks/Pulumi.dev.yaml b/deploy/stacks/Pulumi.dev.yaml new file mode 100644 index 0000000..0d3f52a --- /dev/null +++ b/deploy/stacks/Pulumi.dev.yaml @@ -0,0 +1,10 @@ +# Cluster kind da stack dev, do smoke do CI e do ambiente local. A imagem é +# construída na máquina e carregada no kind, e o salt é o da passphrase vazia, +# porque esta stack não guarda segredo. +config: + markupp:host: markupp.local + markupp:certificateIssuer: selfSigned + markupp:storageClass: standard + markupp:apiReplicas: "1" + markupp:image: markupp:dev +encryptionsalt: v1:tbTefk4lOTE=:v1:LIkxKMN7jxMuArZU:7rDPXfPSUFiFmPLrl91RUTCRM4s6zA== diff --git a/deploy/stacks/Pulumi.prod.yaml b/deploy/stacks/Pulumi.prod.yaml new file mode 100644 index 0000000..6c51d7b --- /dev/null +++ b/deploy/stacks/Pulumi.prod.yaml @@ -0,0 +1,5 @@ +# Enterprise no cluster de produção. O email ACME entra aqui +# quando for escolhido, com pulumi config set. +config: + markupp:host: markupp.negoci.online + markupp:storageClass: longhorn-fast diff --git a/deploy/stacks/Pulumi.yaml b/deploy/stacks/Pulumi.yaml new file mode 100644 index 0000000..f87975a --- /dev/null +++ b/deploy/stacks/Pulumi.yaml @@ -0,0 +1,42 @@ +name: markupp +description: Instalações do markupp que a equipe opera, uma por stack +runtime: yaml + +packages: + markupp: ../pulumi + +config: + namespace: + type: string + default: markupp + image: + type: string + host: + type: string + certificateIssuer: + type: string + default: acme + acmeEmail: + type: string + default: "" + storageClass: + type: string + apiReplicas: + type: integer + default: 2 + +resources: + markupp: + type: markupp:index:Markupp + properties: + namespace: ${namespace} + image: ${image} + host: ${host} + certificateIssuer: ${certificateIssuer} + acmeEmail: ${acmeEmail} + apiReplicas: ${apiReplicas} + database: + storageClass: ${storageClass} + +outputs: + url: ${markupp.url} diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 06dc705..20f9e28 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,64 +1,119 @@ # Deploy Este documento cobre o deploy do **servidor** Markupp, escrito em Go. O servidor expõe a API -REST que os clientes consomem e guarda as notas num PostgreSQL. A configuração vem de variáveis -de ambiente, descritas no [README do servidor](../markupp/README.md#configuração). +REST que os clientes consomem. A imagem é publicada em `ghcr.io/markupp-labs/markupp` a cada +tag `v*`, e a configuração vem de variáveis de ambiente, descritas no +[README do servidor](../markupp/README.md#configuração). -## Pré-requisitos +## Kubernetes -- Docker 24+ com o plugin compose -- Go 1.26, para rodar os testes +A instalação em Kubernetes é o componente Pulumi em `deploy/pulumi` (ADR-0025). Ele cria, no +namespace informado: -## Subir o servidor +- o banco: um PostgreSQL de uma instância no namespace, com senha gerada, ou um Secret com a + URL de um PostgreSQL externo; +- um Job de migração que roda antes da API a cada versão; +- o Deployment e o Service da API; +- um Gateway com HTTPRoute, e um Issuer do cert-manager para o certificado. -O compose sobe o PostgreSQL, aplica as migrações com `markupp migrate` e só então sobe o -servidor: +### Pré-requisitos do cluster + +- Gateway API, com um controlador e uma GatewayClass (padrão `cilium`) +- cert-manager +- o namespace do markupp, já criado +- uma credencial com permissão no namespace para Secret, Job, Deployment, StatefulSet, Service, + Gateway, HTTPRoute e Issuer +- uma StorageClass para o volume do PostgreSQL. Sem informar, vale a padrão do cluster +- um backend de estado do Pulumi + +### Usar o componente + +Em qualquer linguagem do Pulumi, inclusive YAML: ```sh -make run +pulumi package add github.com/markupp-labs/markupp/deploy/pulumi@v1.1.0 +``` + +```yaml +resources: + markupp: + type: markupp:index:Markupp + properties: + namespace: markupp + image: ghcr.io/markupp-labs/markupp:1.1.0 + host: notas.exemplo.com + acmeEmail: ops@exemplo.com + database: + storageClass: longhorn-fast ``` -Servidor disponível em `http://localhost:8080`. Para parar: `docker compose down`, e para -apagar também o banco: `docker compose down -v`. +Sem outra opção, o componente cria um Gateway próprio nas portas 80 e 443. Em cluster onde +outro Gateway já ocupa essas portas, como o Cilium em hostNetwork, que exige porta única por +Gateway, informe `existingGateway` com nome, namespace e listener. O componente cria só o +HTTPRoute, e o Gateway precisa aceitar rotas do namespace do markupp em `allowedRoutes`. Nesse +caso o certificado fica com quem administra o Gateway. -## Validar +### Stacks da equipe -Smoke test pela API: +O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stacks: + +- `prod`: o Enterprise no cluster de produção, com Let's Encrypt +- `dev`: um cluster kind que o CI sobe em cada PR e a cada push em dev para testar a implantação, com + certificado autoassinado + +Para rodar o mesmo teste da stack dev na própria máquina, com Docker, kind, helm, kubectl e +Pulumi instalados: ```sh -ID=$(curl -s -X POST http://localhost:8080/notes \ - -H 'Content-Type: application/json' \ - -d '{"path":"validacao.md","content":"oi"}' \ - | grep -o '"id":"[^"]*"' | cut -d'"' -f4) -curl -s http://localhost:8080/notes/$ID -curl -s -X DELETE http://localhost:8080/notes/$ID -w '%{http_code}\n' +make smoke-kind ``` -Esperado: `POST` retorna JSON com `id` UUID, `GET` traz a nota, `DELETE` responde `204`. Rotas -completas em `markupp/openapi.yaml`. +O smoke usa kubeconfig e estado do Pulumi próprios, num diretório temporário, e apaga o +cluster no fim, passe ou falhe. -Os testes rodam no host e sobem um PostgreSQL descartável com testcontainers: +Para acompanhar o mesmo ambiente de pé, com o painel Headlamp: + +```sh +make kind-up # sobe o cluster, o painel e a stack dev +make kind-painel # mostra o token e abre o painel em http://localhost:4466 +make kind-down # apaga tudo +``` + +O ambiente de pé guarda kubeconfig e estado do Pulumi em `~/.cache/markupp-kind`, sem mexer +no `~/.kube/config`. + +## Desenvolvimento local + +O compose sobe PostgreSQL, aplica as migrações e roda o servidor com recarga automática: + +```sh +make run +``` + +Servidor em `http://localhost:8080`. Os testes rodam no host e precisam de Go e de Docker: ```sh make test ``` -## Build a partir do código fonte +## Validar -Com `MARKUPP_DATABASE_URL` apontando para um PostgreSQL: +Smoke test pela API: ```sh -cd markupp -go build -o markupp ./cmd/markupp -./markupp migrate -./markupp +ID=$(curl -s -X POST http://localhost:8080/notes \ + -H 'Content-Type: application/json' \ + -d '{"path":"validacao.md","content":"oi"}' \ + | grep -o '"id":"[^"]*"' | cut -d'"' -f4) +curl -s http://localhost:8080/notes/$ID +curl -s -X DELETE http://localhost:8080/notes/$ID -w '%{http_code}\n' ``` -A imagem Docker é gerada por `markupp/Dockerfile` (multi-stage, alpine), e o workflow -`.github/workflows/release.yml` publica a imagem a cada tag `v*`. +Esperado: `POST` retorna JSON com `id` UUID, `GET` traz a nota, `DELETE` responde `204`. Rotas +completas em `markupp/openapi.yaml`. ## Aviso -O servidor não tem autenticação ([ADR-0005](adrs/ADR-0005-seguranca-fora-do-mvp.md)). **Não -exponha fora de `localhost`**: qualquer um com acesso à porta consegue ler, criar e apagar -notas. +O servidor não tem autenticação ([ADR-0005](adrs/ADR-0005-seguranca-fora-do-mvp.md)). Não +exponha publicamente antes de ela existir: qualquer um com acesso à porta consegue ler, criar e +apagar notas. diff --git a/docs/adrs/ADR-0018-indexacao-como-funcao-pura.md b/docs/adrs/ADR-0018-indexacao-como-funcao-pura.md index 08616eb..b375fde 100644 --- a/docs/adrs/ADR-0018-indexacao-como-funcao-pura.md +++ b/docs/adrs/ADR-0018-indexacao-como-funcao-pura.md @@ -2,7 +2,7 @@ ## Status -Substituída pelo ADR-0027 +Substituída pelo ADR-0027 e pelo ADR-0033 ## Contexto diff --git a/docs/adrs/ADR-0025-iac-com-pulumi.md b/docs/adrs/ADR-0025-iac-com-pulumi.md index 0bb70cb..e1f2167 100644 --- a/docs/adrs/ADR-0025-iac-com-pulumi.md +++ b/docs/adrs/ADR-0025-iac-com-pulumi.md @@ -6,34 +6,43 @@ Aceita ## Contexto -O Enterprise roda em AWS, com Lambda para indexação, banco gerenciado, armazenamento de objeto, -envio de email e balanceador. Nada disso está descrito em código hoje, e a implantação precisa -ser reproduzível +O Enterprise roda em Kubernetes (ADR-0027), primeiro num cluster on-premise e depois em nuvem +pública. A implantação precisa ser reproduzível pela equipe e por quem hospeda por conta +própria, e nada disso está descrito em código hoje ## Decisão -Pulumi, com os programas escritos em Go, cobrindo apenas a infraestrutura do Enterprise. O -estado fica em bucket S3 da própria conta, não no Pulumi Cloud +Pulumi, com um componente escrito em Go e mantido neste repositório, versionado com a mesma tag +das imagens. O componente declara os recursos Kubernetes do markupp com tipos do SDK, sem chart +Helm por baixo, e é o artefato de instalação em Kubernetes para terceiros. A stack da equipe +consome o mesmo componente + +O estado fica num bucket do Garage, e os segredos da stack são cifrados por passphrase. O CI +implanta com uma credencial restrita ao namespace do markupp. Gateway API e cert-manager são +pré-requisitos do cluster, e KEDA é opcional, instalado à parte por quem administra o cluster ## Alternativas consideradas -- Terraform: mais difundido e com estado explícito, e descreve a infraestrutura em linguagem - própria, separada da linguagem da aplicação -- AWS CDK: o binding de Go é o mais fraco dos disponíveis, porque a ergonomia foi desenhada - para TypeScript -- AWS SAM: cobre bem a parte serverless, e a pilha tem banco, armazenamento, email e - balanceador além do Lambda -- Estado no Pulumi Cloud: menos infraestrutura para montar e histórico pronto, e é gratuito só - para uso individual, além de tirar da conta um arquivo que guarda identificador de cliente -- Publicar módulo para quem hospeda por conta própria: significaria dar suporte a conta, região - e variação de infraestrutura de terceiros, sem receita associada +- Helm chart publicado em OCI: é o formato que terceiros esperam, e obriga manter templates e + schema de valores fora da linguagem do servidor +- Kustomize: YAML sem template, e parametrizar para terceiros vira patch +- Operator próprio: resolve ciclo de vida complexo, e os serviços não têm estado +- Terraform: mais difundido, e descreve a infraestrutura em linguagem própria +- Estado no Pulumi Cloud: menos infraestrutura para montar, e é gratuito só para uso individual +- Estado em bucket de nuvem: sobrevive à perda do cluster, e traz uma conta de nuvem só para isso +- Credencial de administrador do cluster no CI: o CI instalaria os operadores também, e um + vazamento alcançaria os outros projetos do cluster ## Consequências - A infraestrutura fica na mesma linguagem do servidor, com o mesmo lint e a mesma forma de testar +- Reproduzir a instalação exige Pulumi. O componente em Go é consumido de qualquer linguagem do + Pulumi, inclusive YAML, e quem usa Helm, Argo CD ou Flux fica sem artefato oficial +- O Garage roda no cluster, fora de qualquer stack e sem cópia. Perder o cluster leva o estado + junto, e a recuperação é por `pulumi import` - O bucket de estado precisa existir antes da primeira execução, então há um passo manual de origem que não pode ser descrito pelo próprio Pulumi -- O bloqueio de estado precisa estar habilitado no backend, senão duas execuções simultâneas - corrompem o estado -- Quem hospeda por conta própria continua recebendo o compose, sem IaC +- Sem KEDA, os indexadores rodam por CronJob. Sem URL de banco externo, o componente sobe um + Postgres de uma instância no namespace, com senha gerada +- Quem hospeda num nó só continua recebendo o compose, sem IaC diff --git a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md index 5b42b73..349bbb9 100644 --- a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md +++ b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md @@ -17,6 +17,11 @@ Kubernetes é a plataforma de execução do Enterprise e do self-host de porte m mesmas imagens de container usadas no compose. A indexação roda como Job criado sob demanda (ADR-0032), e o TLS termina no Gateway. O compose continua sendo a via para instalação de um nó +A implantação vem em duas fases. Na primeira, o Enterprise roda num cluster kubeadm on-premise, +com Cilium, Gateway API, cert-manager e Longhorn. Na segunda, vai para nuvem pública, com a +mesma instalação. O PostgreSQL roda numa instância só, dentro do namespace do markupp, sem nada +acrescentado no nível do cluster para ele + ## Alternativas consideradas - Manter Lambda e balanceador da AWS: mais barato em volume baixo e sem cluster para operar, ao @@ -24,20 +29,25 @@ mesmas imagens de container usadas no compose. A indexação roda como Job criad conexão obrigatório, teto de quinze minutos e cold start - Só Kubernetes, sem compose: uma forma de implantar e menos para manter, e afasta o self-hoster individual, que é justamente quem roda compose +- PostgreSQL com CloudNativePG em duas instâncias: failover automático e backup gerenciado, ao + custo de CRD, webhook e operador no cluster compartilhado com outros projetos - Nomad ou Docker Swarm: menos operação que Kubernetes, com ecossistema e disponibilidade de mão de obra muito menores ## Consequências -- O que a equipe opera na nuvem passa a ser uma instalação que qualquer pessoa reproduz +- O que a equipe opera passa a ser uma instalação que qualquer pessoa com Pulumi reproduz + (ADR-0025). Quem usa Helm, Argo CD ou Flux fica sem artefato oficial - Some a taxa do Lambda: sem pool obrigatório na frente do Postgres, sem teto de quinze minutos e sem fatiar lote por causa de tempo -- A indexação pode rodar em nó interrompível, porque o plano de recuperação é descartável por - decisão (ADR-0012): perder o nó no meio custa reprocessar, não perder dado -- Substitui o ADR-0018 e o ADR-0023. O self-host de um nó continua terminando TLS no próprio +- Substitui o ADR-0023 e a forma de invocação do ADR-0018. A indexação como função pura + continua valendo, pelo ADR-0033. O self-host de um nó continua terminando TLS no próprio servidor com Let's Encrypt, como o ADR-0023 definia - Operar Kubernetes é trabalho novo para uma equipe pequena, e o cluster tem piso de custo que o Lambda não tinha +- Na primeira fase o backup do banco é snapshot de volume no próprio cluster. Perder o cluster + leva as notas junto +- O banco não tem failover: se o nó dele cair, a API fica sem banco até o pod subir em outro nó - A entrada de tráfego usa Gateway API, e não Ingress. A API Ingress está congelada e a documentação do Kubernetes recomenda Gateway no lugar dela. São três recursos com donos distintos, GatewayClass, Gateway e HTTPRoute, e o cluster precisa dos CRDs e de um controlador diff --git a/docs/adrs/ADR-0033-indexador-como-funcao-pura.md b/docs/adrs/ADR-0033-indexador-como-funcao-pura.md new file mode 100644 index 0000000..1f74314 --- /dev/null +++ b/docs/adrs/ADR-0033-indexador-como-funcao-pura.md @@ -0,0 +1,33 @@ +# ADR-0033: Indexador como função pura + +## Status + +Aceita + +## Contexto + +O ADR-0027 substituiu o ADR-0018 inteiro, e com ele foi embora uma decisão que não dependia do +Lambda: o indexador não sabe quem o invocou. Com Job no cluster, laço no compose e KEDA +disparando (ADR-0032), existem várias formas de invocar a mesma indexação + +## Decisão + +O indexador é uma função de (tenant, conjunto de mudanças) para linhas derivadas, sem estado +próprio e sem saber quem o invocou. O conjunto de mudanças vem do cursor de revisão. Cada forma +de invocação é um entrypoint fino sobre o mesmo pacote: execução única no Job do cluster e laço +no compose + +## Alternativas consideradas + +- Indexador ciente da plataforma, lendo variáveis do Job ou falando com a API do Kubernetes: + menos camadas, e cada edição passa a ter uma indexação própria +- Deixar a pureza implícita, sem registro: nada impede o próximo entrypoint de carregar estado + para dentro do pacote + +## Consequências + +- Uma implementação de indexação serve o cluster e o compose, sem edição com funcionalidade a + menos +- O indexador é testável sem Kubernetes, com um conjunto de mudanças montado no teste +- Trocar a forma de disparo, de KEDA para CronJob ou para broker, não toca o pacote de indexação +- Indexação atrasada não impede leitura nem escrita de nota, como o ADR-0012 prometeu diff --git a/docs/diagrama-de-implantacao.drawio.svg b/docs/diagrama-de-implantacao.drawio.svg new file mode 100644 index 0000000..8c827ba --- /dev/null +++ b/docs/diagrama-de-implantacao.drawio.svg @@ -0,0 +1,4 @@ + + + +Cluster de produção (Kubernetes)Pré-requisitos que já existem no cluster: GatewayClass cilium (Gateway API), cert-manager com --enable-gateway-api, StorageClass longhorn-fast (Longhorn)namespace markuppGateway markupp-gatewayhttps 443, http 80HTTPRoute markupp-apiService markupp-apiDeployment markupp-api2 réplicasIssuer markupp-issuerLet's EncryptSecret markupp-tlscertificadoJob markupp-migrateantes de cada versãoSecret markupp-db-appsenha gerada e URLService markupp-dbheadlessStatefulSet markupp-dbPostgreSQL 17, 1 instânciaPVC datalonghorn-fastClientesplugin, agente, painelLet's EncryptPulumi, stack prod(deploy/stacks)HTTPS 443markupp.negoci.onlineHTTP 8080TCP 5432TCP 5432emitetermina o TLSACME HTTP-01porta 80lê URL e senhacria e atualiza \ No newline at end of file diff --git a/docs/diagrama-de-implantacao.md b/docs/diagrama-de-implantacao.md new file mode 100644 index 0000000..39add3a --- /dev/null +++ b/docs/diagrama-de-implantacao.md @@ -0,0 +1,38 @@ +# Diagrama de implantação + +![Implantação do Enterprise no cluster de produção](diagrama-de-implantacao.drawio.svg) + +Diagrama de implantação no estilo C4, com os ícones do Kubernetes. Mostra o que a stack prod +cria no cluster de produção e o que ela usa sem criar. O desenho por serviço está nos +[diagramas de blocos](diagramas-de-blocos.md). + +## O que já existe no cluster + +GatewayClass do Cilium, cert-manager e a StorageClass longhorn-fast já estão instalados e são +usados por outros projetos. O markupp só os usa: tudo o que ele cria fica dentro do namespace +markupp, e nada muda na configuração do cluster. + +## Entrada + +O cliente chega por HTTPS em markupp.negoci.online. O Gateway termina o TLS e o HTTPRoute +encaminha para o Service da API. O certificado vem do Issuer, que valida o domínio no Let's +Encrypt pela porta 80 e grava o resultado no Secret markupp-tls. + +## API e banco + +A API roda como Deployment de duas réplicas e fala com o PostgreSQL pelo Service markupp-db. O +banco é um StatefulSet de uma instância, com o volume num PVC da longhorn-fast. Sem failover: +se o nó do banco cair, a API fica sem banco até o pod subir em outro nó. + +O Secret markupp-db-app guarda a senha gerada pelo Pulumi e a URL de conexão. A API e o Job +leem a URL, e o PostgreSQL lê a senha. + +## Ordem de subida + +O Pulumi cria o banco, espera ele aceitar conexão, roda o Job de migração até ele terminar, e +só então atualiza a API. O schema muda antes de qualquer réplica nova atender. + +## Na stack dev + +A stack dev, no kind do CI e do make kind-up, cria os mesmos recursos. Mudam três coisas: o +certificado é autoassinado, o volume usa a StorageClass do kind, e a API tem uma réplica. diff --git a/docs/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index 27ce1d4..6ed6c9f 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -54,15 +54,18 @@ Deployment e não Job, porque o KEDA leva as réplicas a zero sem precisar de um Um PostgreSQL só, com o plano de registro e o plano de recuperação dentro dele. Os vetores ficam em pgvector. -Ele roda dentro do cluster, com volume persistente, e não como banco gerenciado de nuvem. É o -que o ADR-0015 pede sem dizer onde, e é o que a infraestrutura disponível oferece. +Ele roda numa instância só, dentro do namespace do markupp, e não como banco gerenciado de +nuvem nem com operador no cluster compartilhado. É o que o ADR-0015 pede sem dizer onde, e é o +que a infraestrutura disponível oferece. O backup é snapshot de volume no próprio cluster, então perder o cluster leva as notas +junto. Seis dos sete serviços compartilham esse banco. O painel fica de fora porque consome a API, e não o PostgreSQL. São serviços no sentido de implantação e escala independentes, não no de banco por serviço, e o isolamento entre eles é de processo e de escala, não de dado. O preço é acoplamento pelo schema. Mudança de coluna coordena os seis, em expand e contract, e -nenhum deles evolui o modelo de dados por conta própria. +nenhum deles evolui o modelo de dados por conta própria. Quem aplica a migração é um Job único, +que roda antes de qualquer serviço subir a versão nova. ## Provedores externos @@ -70,13 +73,15 @@ A API REST valida identidade contra o provedor OIDC habilitado, o indexador de e gera vetores pelo provedor configurado, e o worker de notificação entrega email pelo dele. Os três são trocáveis por configuração, e é por isso que o desenho não nomeia serviço. O -Enterprise liga Bedrock e SES, o self-host aponta para um modelo local e um SMTP próprio, e +Enterprise entrega email por SMTP, o self-host aponta para um modelo local e um SMTP próprio, e o código é o mesmo nos dois. ## O que muda no self-host -O mesmo conjunto de imagens roda por compose num nó só. Sem cluster, o TLS termina no próprio -servidor, e o embedding pode vir de modelo local carregado no processo do indexador. +O mesmo conjunto de imagens roda por compose num nó só, menos o plano de controle: com um +tenant só, não há cobrança nem provisionamento. Embedding e notificação sobem por perfil, só +quando configurados. Sem cluster, o TLS termina no próprio servidor, e o embedding pode vir de +modelo local carregado no processo do indexador. Sem cluster também não há KEDA, então lá o indexador roda em laço em vez de nascer por lote. diff --git a/docs/embedding.md b/docs/embedding.md index fad1d17..07e67b8 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -1,6 +1,6 @@ # Embedding -Configuração planejada, ainda não implementada. A razão de ser plugável está no ADR-0019. +Configuração planejada, ainda não implementada. A razão de ser plugável está no ADR-0029. ## Escolha rápida @@ -93,7 +93,7 @@ Alternativa com mais qualidade e mais RAM: `mxbai-embed-large`, 1024 dimensões. ## bedrock -Padrão do Enterprise. Sem chave no arquivo: a credencial vem do papel de execução da Lambda. +Padrão do Enterprise. Sem chave no arquivo: a credencial vem da identidade do pod. ```json { diff --git a/markupp/CONTEXT.md b/markupp/CONTEXT.md index 2849281..5caa5b0 100644 --- a/markupp/CONTEXT.md +++ b/markupp/CONTEXT.md @@ -11,6 +11,15 @@ e agente de IA são clientes, e nenhum deles é privilegiado. O navegador consom o markupp. _Avoid_: consumidor, integração, aplicação externa, frontend +**Enterprise**: +A edição que a equipe opera, com vários tenants. O nome diz quem opera, não onde roda. +_Avoid_: nuvem, SaaS, hospedado + +**Self-host**: +A edição que qualquer pessoa instala e opera por conta própria, com um tenant só e sem +plano de controle. +_Avoid_: on-premise, comunidade, open source + **Nota**: A unidade de conteúdo markdown que o cliente cria, lê, edita e apaga. _Avoid_: anotação, documento, arquivo, página