From 448567a840728c2b9443206dba30de8a34328edd Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:06:10 -0300 Subject: [PATCH 01/47] docs: define enterprise e self-host no glossario --- markupp/CONTEXT.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From 455a8a1a60715a8da5feb3c3a3a60677d1a250fc Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:06:43 -0300 Subject: [PATCH 02/47] docs: registra o indexador como funcao pura na adr-0033 --- .../ADR-0018-indexacao-como-funcao-pura.md | 2 +- .../ADR-0033-indexador-como-funcao-pura.md | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 docs/adrs/ADR-0033-indexador-como-funcao-pura.md 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-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 From e59f18b2a2cafe29e8bf86a91092591101791c3f Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:06:44 -0300 Subject: [PATCH 03/47] docs: poe as fases on-premise e nuvem na adr-0027 --- docs/adrs/ADR-0027-kubernetes-como-plataforma.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md index 5b42b73..f8d3e2a 100644 --- a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md +++ b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md @@ -17,6 +17,10 @@ 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 no cluster kubeadm do campus +São José do IFSC, com Cilium, Gateway API, cert-manager e Longhorn. Na segunda, vai para nuvem +pública, com a mesma instalação. O PostgreSQL roda dentro do cluster, com CloudNativePG + ## Alternativas consideradas - Manter Lambda e balanceador da AWS: mais barato em volume baixo e sem cluster para operar, ao @@ -29,15 +33,17 @@ mesmas imagens de container usadas no compose. A indexação roda como Job criad ## 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 - 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 From 739fb3c128df0c65ada9f1701e430df0cb4ada06 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:07:03 -0300 Subject: [PATCH 04/47] docs: troca a aws pelo componente pulumi do cluster na adr-0025 --- docs/adrs/ADR-0025-iac-com-pulumi.md | 46 +++++++++++++++++----------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/adrs/ADR-0025-iac-com-pulumi.md b/docs/adrs/ADR-0025-iac-com-pulumi.md index 0bb70cb..842ce18 100644 --- a/docs/adrs/ADR-0025-iac-com-pulumi.md +++ b/docs/adrs/ADR-0025-iac-com-pulumi.md @@ -6,34 +6,44 @@ 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 no cluster do IFSC 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 e CloudNativePG são opcionais, instalados à 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 CloudNativePG, o componente recebe a conexão de + um Postgres qualquer +- Quem hospeda num nó só continua recebendo o compose, sem IaC From bc919f02ee32de6ebb51ebefddf89a133ac91647 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:07:50 -0300 Subject: [PATCH 05/47] docs: aponta o embedding para a adr-0029 --- docs/embedding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/embedding.md b/docs/embedding.md index fad1d17..c55d023 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 From f2c7ed3f7970227c6ea1a9981d9a895d6a8b910f Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:07:50 -0300 Subject: [PATCH 06/47] docs: tira o papel da lambda da credencial do bedrock --- docs/embedding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/embedding.md b/docs/embedding.md index c55d023..07e67b8 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -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 { From 8b97cb53cb765fe42231f6915e0650c68844a192 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:08:35 -0300 Subject: [PATCH 07/47] docs: poe o cloudnativepg e o backup por snapshot no diagrama --- docs/diagramas-de-blocos.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index 27ce1d4..c070f01 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -54,8 +54,10 @@ 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 dentro do cluster, com CloudNativePG em duas instâncias, 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. 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 From d7f1132aed74ba309e6496e3d6de940a6bbfb2b9 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:08:36 -0300 Subject: [PATCH 08/47] docs: nomeia o job de migracao no diagrama --- docs/diagramas-de-blocos.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index c070f01..ca04132 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -64,7 +64,8 @@ não o PostgreSQL. São serviços no sentido de implantação e escala independe 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 From 403de061b52be660540097b1d19d3416340ac229 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:08:36 -0300 Subject: [PATCH 09/47] docs: troca o ses por smtp no enterprise --- docs/diagramas-de-blocos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index ca04132..050fc49 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -73,7 +73,7 @@ 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 From bf3c09855d03a1a9c4608a007f380bf22e4432aa Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 25 Sep 2026 20:08:37 -0300 Subject: [PATCH 10/47] docs: tira o plano de controle do self-host no diagrama --- docs/diagramas-de-blocos.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index 050fc49..e782d17 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -78,8 +78,10 @@ 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. From 9bc0f6534abd0e633686d198eeec5dee1e9953e2 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 08:47:05 -0300 Subject: [PATCH 11/47] test: espera o componente pulumi com api, banco e entrada --- deploy/pulumi/go.mod | 122 ++++++++++++ deploy/pulumi/go.sum | 339 ++++++++++++++++++++++++++++++++++ deploy/pulumi/markupp_test.go | 173 +++++++++++++++++ 3 files changed, 634 insertions(+) create mode 100644 deploy/pulumi/go.mod create mode 100644 deploy/pulumi/go.sum create mode 100644 deploy/pulumi/markupp_test.go diff --git a/deploy/pulumi/go.mod b/deploy/pulumi/go.mod new file mode 100644 index 0000000..cced8c4 --- /dev/null +++ b/deploy/pulumi/go.mod @@ -0,0 +1,122 @@ +module github.com/markupp-labs/markupp/deploy/pulumi + +go 1.26.6 + +require ( + 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/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // 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.2 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // 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.5.0 // indirect + github.com/djherbis/times v1.5.0 // indirect + github.com/ebitengine/purego v0.10.2 // 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.1 // indirect + github.com/go-git/go-git/v6 v6.0.0-alpha.4 // 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.29.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.8.0 // indirect + github.com/hashicorp/hcl/v2 v2.22.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.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.20 // 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/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/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.20260918.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // 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 v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/zalando/go-keyring v0.2.8 // indirect + github.com/zclconf/go-cty v1.13.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/collector/featuregate v1.53.0 // indirect + go.opentelemetry.io/collector/pdata v1.53.0 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect + go.opentelemetry.io/otel/log v0.21.0 // indirect + go.opentelemetry.io/otel/metric v1.45.0 // indirect + go.opentelemetry.io/otel/sdk v1.45.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.56.0 // indirect + golang.org/x/mod v0.40.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.49.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.2 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/frand v1.4.2 // indirect +) diff --git a/deploy/pulumi/go.sum b/deploy/pulumi/go.sum new file mode 100644 index 0000000..c9f7b64 --- /dev/null +++ b/deploy/pulumi/go.sum @@ -0,0 +1,339 @@ +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/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +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/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +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.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA= +github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +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/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.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ= +github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E= +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.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ= +github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc= +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/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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +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.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +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/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/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.20260918.0 h1:uq7VYDSrWGfQZbyUKr9emUbqRy1RsSnaFQ67mlcLBWU= +github.com/pulumi/pulumi-cloud-sdk/go v1.20260918.0/go.mod h1:NPA79X6Wmyr5zNKd+N+RiVJN8duwIrRmIaed8HCL9EM= +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/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/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.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 v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +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.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= +go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= +go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU= +go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= +go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 h1:oEl2Pw/i4OQwhAuda2pAHFAcOMivA+Xa+iTccBfab/g= +go.opentelemetry.io/contrib/bridges/otelslog v0.20.0/go.mod h1:yMSQaiiq5dpfrSJCYLBcqFeJkFFI67seT4ngvx6jfVo= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA= +go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc= +go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ= +go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +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.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= +go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +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.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20190626221950-04f50cda93cb/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +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-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/deploy/pulumi/markupp_test.go b/deploy/pulumi/markupp_test.go new file mode 100644 index 0000000..172b9bc --- /dev/null +++ b/deploy/pulumi/markupp_test.go @@ -0,0 +1,173 @@ +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" +) + +// fakeResourceMonitor faz o papel do motor do Pulumi: aceita cada recurso +// declarado e guarda os inputs para o teste inspecionar. +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) + return args.Name + "-id", args.Inputs, 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-single"}, + } +} + +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 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 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 := pulumi.RunErr(func(ctx *pulumi.Context) error { + _, err := NewMarkupp(ctx, "markupp", args) + return err + }, pulumi.WithMocks("markupp", "teste", &fakeResourceMonitor{})) + + require.Error(t, err) + assert.Contains(t, err.Error(), "namespace") +} + +func TestNewMarkupp_SemBancoExterno_CriaClusterCloudNativePG(t *testing.T) { + monitor := deploy(t, argsDeTeste()) + + cluster := monitor.single(t, "kubernetes:postgresql.cnpg.io/v1:Cluster") + assert.Equal(t, 2.0, at(t, cluster, "spec", "instances").NumberValue()) + assert.Equal(t, "longhorn-single", at(t, cluster, "spec", "storage", "storageClass").StringValue()) + assert.Equal(t, "markupp", at(t, cluster, "metadata", "namespace").StringValue()) +} + +func TestNewMarkupp_ComBancoExterno_NaoCriaClusterEGuardaURLEmSecret(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:postgresql.cnpg.io/v1:Cluster")) + 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_LeOBancoDoSecretDoCluster(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()) +} From 7a4a2c04a911044d1783a0e79755635d202a5c5e Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 08:57:33 -0300 Subject: [PATCH 12/47] feat: adiciona o componente pulumi com api, banco e entrada --- deploy/pulumi/PulumiPlugin.yaml | 1 + deploy/pulumi/database.go | 63 +++++++++ deploy/pulumi/go.mod | 93 +++++++------ deploy/pulumi/go.sum | 233 ++++++++++++++++++-------------- deploy/pulumi/ingress.go | 95 +++++++++++++ deploy/pulumi/main.go | 27 ++++ deploy/pulumi/markupp.go | 135 ++++++++++++++++++ deploy/pulumi/workload.go | 92 +++++++++++++ 8 files changed, 592 insertions(+), 147 deletions(-) create mode 100644 deploy/pulumi/PulumiPlugin.yaml create mode 100644 deploy/pulumi/database.go create mode 100644 deploy/pulumi/ingress.go create mode 100644 deploy/pulumi/main.go create mode 100644 deploy/pulumi/markupp.go create mode 100644 deploy/pulumi/workload.go 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..2ce3d92 --- /dev/null +++ b/deploy/pulumi/database.go @@ -0,0 +1,63 @@ +package main + +import ( + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes" + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" + 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" +) + +// dbSecretKey é a chave com a URL de conexão. O CloudNativePG já grava a URL +// com esse nome no Secret -app, e o banco externo segue o mesmo +// formato para a API ler de um lugar só. +const dbSecretKey = "uri" + +// declareDatabase cria o banco 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 declareCloudNativePG(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) +} + +func declareCloudNativePG(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) (pulumi.Resource, error) { + storage := pulumi.Map{"size": pulumi.String(args.Database.Size)} + if args.Database.StorageClass != "" { + storage["storageClass"] = pulumi.String(args.Database.StorageClass) + } + return apiextensions.NewCustomResource(ctx, names.database, &apiextensions.CustomResourceArgs{ + ApiVersion: pulumi.String("postgresql.cnpg.io/v1"), + Kind: pulumi.String("Cluster"), + Metadata: namespaced(names.database, args.Namespace), + OtherFields: kubernetes.UntypedArgs{"spec": pulumi.Map{ + "instances": pulumi.Int(args.Database.Instances), + "storage": storage, + "bootstrap": pulumi.Map{"initdb": pulumi.Map{"database": pulumi.String("markupp"), "owner": pulumi.String("markupp")}}, + }}, + }, parent) +} + +func databaseURLEnv(names resourceNames) corev1.EnvVarArray { + return corev1.EnvVarArray{corev1.EnvVarArgs{ + Name: pulumi.String("MARKUPP_DATABASE_URL"), + ValueFrom: corev1.EnvVarSourceArgs{SecretKeyRef: corev1.SecretKeySelectorArgs{ + Name: pulumi.String(names.dbSecret), + Key: pulumi.String(dbSecretKey), + }}, + }} +} + +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 index cced8c4..d97fa4e 100644 --- a/deploy/pulumi/go.mod +++ b/deploy/pulumi/go.mod @@ -3,6 +3,8 @@ 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/sdk/v3 v3.265.0 github.com/stretchr/testify v1.12.1 ) @@ -11,9 +13,7 @@ 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/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // 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 @@ -21,23 +21,24 @@ require ( 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.2 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // 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.5.0 // 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.1 // indirect - github.com/go-git/go-git/v6 v6.0.0-alpha.4 // 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 @@ -46,21 +47,21 @@ require ( 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.29.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.8.0 // indirect - github.com/hashicorp/hcl/v2 v2.22.0 // 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.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // 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.20 // 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 @@ -68,55 +69,63 @@ require ( 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.20260918.0 // 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 v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xo/terminfo v1.2.0 // indirect github.com/zalando/go-keyring v0.2.8 // indirect - github.com/zclconf/go-cty v1.13.2 // 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.53.0 // indirect - go.opentelemetry.io/collector/pdata v1.53.0 // indirect - go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 // indirect - go.opentelemetry.io/otel v1.45.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect - go.opentelemetry.io/otel/log v0.21.0 // indirect - go.opentelemetry.io/otel/metric v1.45.0 // indirect - go.opentelemetry.io/otel/sdk v1.45.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect - go.opentelemetry.io/otel/trace v1.45.0 // 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.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.56.0 // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.49.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect - google.golang.org/grpc v1.83.2 // indirect - google.golang.org/protobuf v1.36.11 // 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.4.2 // indirect + lukechampine.com/frand v1.5.1 // indirect ) diff --git a/deploy/pulumi/go.sum b/deploy/pulumi/go.sum index c9f7b64..8035afe 100644 --- a/deploy/pulumi/go.sum +++ b/deploy/pulumi/go.sum @@ -6,14 +6,10 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo 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/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 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/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 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= @@ -30,12 +26,12 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f 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.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= -github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +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= @@ -50,14 +46,17 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA= -github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +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= @@ -66,12 +65,12 @@ 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.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ= -github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E= +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.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ= -github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc= +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= @@ -81,6 +80,8 @@ 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= @@ -97,8 +98,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E 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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +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= @@ -106,10 +107,10 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY 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.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= -github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +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= @@ -127,17 +128,16 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN 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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +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= @@ -154,6 +154,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU 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= @@ -163,6 +169,8 @@ 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= @@ -172,8 +180,14 @@ 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.20260918.0 h1:uq7VYDSrWGfQZbyUKr9emUbqRy1RsSnaFQ67mlcLBWU= -github.com/pulumi/pulumi-cloud-sdk/go v1.20260918.0/go.mod h1:NPA79X6Wmyr5zNKd+N+RiVJN8duwIrRmIaed8HCL9EM= +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/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= @@ -181,8 +195,14 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc 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= @@ -196,6 +216,7 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q 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= @@ -204,56 +225,60 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO 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 v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +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.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= -go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= -go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU= -go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= -go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= -go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= -go.opentelemetry.io/contrib/bridges/otelslog v0.20.0 h1:oEl2Pw/i4OQwhAuda2pAHFAcOMivA+Xa+iTccBfab/g= -go.opentelemetry.io/contrib/bridges/otelslog v0.20.0/go.mod h1:yMSQaiiq5dpfrSJCYLBcqFeJkFFI67seT4ngvx6jfVo= -go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= -go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= -go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA= -go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g= -go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= -go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= -go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= -go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc= -go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo= -go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE= -go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= -go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= -go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= -go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +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.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= -go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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= @@ -264,66 +289,63 @@ 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.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= -golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +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.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20190626221950-04f50cda93cb/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +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-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= -google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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= @@ -331,9 +353,10 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV 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.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= -lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= -pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= -pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +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..82a97fc --- /dev/null +++ b/deploy/pulumi/ingress.go @@ -0,0 +1,95 @@ +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. +// 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 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, parent) +} + +// declareIssuer resolve o desafio ACME HTTP-01 pelo listener http do próprio +// Gateway, como o cert-manager faz com Gateway API. +func declareIssuer(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { + solver := pulumi.Map{"http01": pulumi.Map{"gatewayHTTPRoute": pulumi.Map{ + "parentRefs": pulumi.Array{gatewayRef(names, args, httpListener)}, + }}} + return declareCustom(ctx, "cert-manager.io/v1", "Issuer", names.issuer, args, 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}, + }, + }, parent) +} + +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, 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{gatewayRef(names, args, httpsListener)}, + "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 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..d12e7df --- /dev/null +++ b/deploy/pulumi/markupp.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +const ( + componentType = "markupp:index:Markupp" + defaultGatewayClass = "cilium" + defaultAPIReplicas = 2 + defaultDBInstances = 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"` + // AcmeEmail recebe os avisos do Let's Encrypt. + AcmeEmail string `pulumi:"acmeEmail"` + // GatewayClassName é a classe do Gateway. Padrão cilium. + GatewayClassName string `pulumi:"gatewayClassName,optional"` + // APIReplicas é o número de réplicas da API. Padrão 2. + APIReplicas int `pulumi:"apiReplicas,optional"` + // Database define de onde vem o PostgreSQL. + Database DatabaseArgs `pulumi:"database"` +} + +// DatabaseArgs escolhe entre um PostgreSQL externo e um cluster CloudNativePG. +type DatabaseArgs struct { + // ExternalURL aponta para um PostgreSQL já existente. Vazio cria um + // cluster CloudNativePG, que exige o operador instalado. + ExternalURL pulumi.StringInput `pulumi:"externalUrl,optional" provider:"secret"` + // Instances é o número de instâncias do CloudNativePG. Padrão 2. + Instances int `pulumi:"instances,optional"` + // StorageClass dos volumes do CloudNativePG. + StorageClass string `pulumi:"storageClass,optional"` + // Size de cada volume do CloudNativePG. 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) +} + +func validate(args MarkuppArgs) error { + required := []struct{ field, value string }{ + {"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}, {"acmeEmail", args.AcmeEmail}, + } + for _, r := range required { + if r.value == "" { + return fmt.Errorf("campo %s vazio, esperado texto não vazio (namespace=%q image=%q host=%q)", + r.field, args.Namespace, args.Image, args.Host) + } + } + return nil +} + +func withDefaults(args MarkuppArgs) MarkuppArgs { + if args.GatewayClassName == "" { + args.GatewayClassName = defaultGatewayClass + } + if args.APIReplicas == 0 { + args.APIReplicas = defaultAPIReplicas + } + if args.Database.Instances == 0 { + args.Database.Instances = defaultDBInstances + } + 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/workload.go b/deploy/pulumi/workload.go new file mode 100644 index 0000000..8207149 --- /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 CloudNativePG 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)} +} From 7ebd0e570c810430541cf2657d2dcce5c2080b35 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 08:59:51 -0300 Subject: [PATCH 13/47] feat: adiciona a stack pulumi do cluster de producao --- deploy/ifsc/Pulumi.yaml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 deploy/ifsc/Pulumi.yaml diff --git a/deploy/ifsc/Pulumi.yaml b/deploy/ifsc/Pulumi.yaml new file mode 100644 index 0000000..5b4deaa --- /dev/null +++ b/deploy/ifsc/Pulumi.yaml @@ -0,0 +1,33 @@ +name: markupp-ifsc +description: Enterprise do markupp no cluster do campus São José do IFSC +runtime: yaml + +packages: + markupp: ../pulumi + +config: + namespace: + type: string + default: markupp + image: + type: string + host: + type: string + acmeEmail: + type: string + storageClass: + type: string + +resources: + markupp: + type: markupp:index:Markupp + properties: + namespace: ${namespace} + image: ${image} + host: ${host} + acmeEmail: ${acmeEmail} + database: + storageClass: ${storageClass} + +outputs: + url: ${markupp.url} From 189b0374692bc0c4ed83d77841316c529fc693a2 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 09:01:15 -0300 Subject: [PATCH 14/47] docs: reescreve o deploy para kubernetes e compose com postgres --- docs/DEPLOY.md | 89 +++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 06dc705..1eeb9ae 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,25 +1,68 @@ # 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 cluster CloudNativePG, 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, Service, Gateway, + HTTPRoute, Issuer e, com CloudNativePG, Cluster +- opcional: o operador CloudNativePG e uma StorageClass para os volumes dele. Sem ele, informe + `database.externalUrl` +- um backend de estado do Pulumi + +### Usar o componente + +Em qualquer linguagem do Pulumi, inclusive YAML: + +```sh +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-single +``` + +A stack da equipe, no cluster do IFSC, está em `deploy/ifsc` e usa o componente pelo caminho +local. + +## Desenvolvimento local + +O compose sobe PostgreSQL, aplica as migrações e roda o servidor com recarga automática: ```sh make run ``` -Servidor disponível em `http://localhost:8080`. Para parar: `docker compose down`, e para -apagar também o banco: `docker compose down -v`. +Servidor em `http://localhost:8080`. Os testes rodam no host e precisam de Go e de Docker: + +```sh +make test +``` ## Validar @@ -37,28 +80,8 @@ curl -s -X DELETE http://localhost:8080/notes/$ID -w '%{http_code}\n' Esperado: `POST` retorna JSON com `id` UUID, `GET` traz a nota, `DELETE` responde `204`. Rotas completas em `markupp/openapi.yaml`. -Os testes rodam no host e sobem um PostgreSQL descartável com testcontainers: - -```sh -make test -``` - -## Build a partir do código fonte - -Com `MARKUPP_DATABASE_URL` apontando para um PostgreSQL: - -```sh -cd markupp -go build -o markupp ./cmd/markupp -./markupp migrate -./markupp -``` - -A imagem Docker é gerada por `markupp/Dockerfile` (multi-stage, alpine), e o workflow -`.github/workflows/release.yml` publica a imagem a cada tag `v*`. - ## 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. From edf53d812d68b07a67b56b2a0d40be001f90c84f Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 09:01:45 -0300 Subject: [PATCH 15/47] chore: roda os testes do componente pulumi no ci --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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 From 903b9c433d4b922c5a170503dd4849199d392548 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 20:57:32 -0300 Subject: [PATCH 16/47] test: espera o emissor autoassinado como opcao do componente --- deploy/pulumi/markupp_test.go | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/deploy/pulumi/markupp_test.go b/deploy/pulumi/markupp_test.go index 172b9bc..56115fc 100644 --- a/deploy/pulumi/markupp_test.go +++ b/deploy/pulumi/markupp_test.go @@ -68,6 +68,13 @@ func deploy(t *testing.T, args MarkuppArgs) *fakeResourceMonitor { 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) @@ -92,10 +99,7 @@ func TestNewMarkupp_SemNamespace_RetornaErroComOCampo(t *testing.T) { args := argsDeTeste() args.Namespace = "" - err := pulumi.RunErr(func(ctx *pulumi.Context) error { - _, err := NewMarkupp(ctx, "markupp", args) - return err - }, pulumi.WithMocks("markupp", "teste", &fakeResourceMonitor{})) + err := declareErr(args) require.Error(t, err) assert.Contains(t, err.Error(), "namespace") @@ -171,3 +175,36 @@ func TestNewMarkupp_Issuer_UsaOEmailACMEInformado(t *testing.T) { 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") +} + +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") +} From 3525da4da14a9e073bd01581342bed0130bf01a1 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 20:58:06 -0300 Subject: [PATCH 17/47] feat: aceita emissor autoassinado no componente pulumi --- deploy/pulumi/ingress.go | 26 ++++++++++++++++---------- deploy/pulumi/markupp.go | 21 ++++++++++++++++++--- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/deploy/pulumi/ingress.go b/deploy/pulumi/ingress.go index 82a97fc..fce4062 100644 --- a/deploy/pulumi/ingress.go +++ b/deploy/pulumi/ingress.go @@ -25,20 +25,26 @@ func declareIngress(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, return declareRoute(ctx, names, args, parent) } -// declareIssuer resolve o desafio ACME HTTP-01 pelo listener http do próprio -// Gateway, como o cert-manager faz com Gateway API. 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 declareCustom(ctx, "cert-manager.io/v1", "Issuer", names.issuer, args, 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}, - }, - }, parent) + 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 { diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go index d12e7df..7769e8b 100644 --- a/deploy/pulumi/markupp.go +++ b/deploy/pulumi/markupp.go @@ -9,6 +9,8 @@ import ( const ( componentType = "markupp:index:Markupp" defaultGatewayClass = "cilium" + issuerACME = "acme" + issuerSelfSigned = "selfSigned" defaultAPIReplicas = 2 defaultDBInstances = 2 defaultDBSize = "10Gi" @@ -32,8 +34,11 @@ type MarkuppArgs struct { Image string `pulumi:"image"` // Host é o domínio público da API. Host string `pulumi:"host"` - // AcmeEmail recebe os avisos do Let's Encrypt. - AcmeEmail string `pulumi:"acmeEmail"` + // 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"` // APIReplicas é o número de réplicas da API. Padrão 2. @@ -90,7 +95,14 @@ func declareResources(ctx *pulumi.Context, name string, args MarkuppArgs, parent func validate(args MarkuppArgs) error { required := []struct{ field, value string }{ - {"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}, {"acmeEmail", args.AcmeEmail}, + {"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}, + } + switch args.CertificateIssuer { + case "", issuerACME: + required = append(required, struct{ field, value string }{"acmeEmail", args.AcmeEmail}) + case issuerSelfSigned: + default: + return fmt.Errorf("certificateIssuer=%q, esperado %q ou %q", args.CertificateIssuer, issuerACME, issuerSelfSigned) } for _, r := range required { if r.value == "" { @@ -102,6 +114,9 @@ func validate(args MarkuppArgs) error { } func withDefaults(args MarkuppArgs) MarkuppArgs { + if args.CertificateIssuer == "" { + args.CertificateIssuer = issuerACME + } if args.GatewayClassName == "" { args.GatewayClassName = defaultGatewayClass } From 9c9380843b3cd0ac79d6b2ba7b1e2dda6e5c3c3c Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 20:58:20 -0300 Subject: [PATCH 18/47] refact: generaliza a stack de producao num projeto com uma stack por ambiente --- deploy/{ifsc => stacks}/Pulumi.yaml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) rename deploy/{ifsc => stacks}/Pulumi.yaml (56%) diff --git a/deploy/ifsc/Pulumi.yaml b/deploy/stacks/Pulumi.yaml similarity index 56% rename from deploy/ifsc/Pulumi.yaml rename to deploy/stacks/Pulumi.yaml index 5b4deaa..faefe2d 100644 --- a/deploy/ifsc/Pulumi.yaml +++ b/deploy/stacks/Pulumi.yaml @@ -1,5 +1,5 @@ -name: markupp-ifsc -description: Enterprise do markupp no cluster do campus São José do IFSC +name: markupp +description: Instalações do markupp que a equipe opera, uma por stack runtime: yaml packages: @@ -13,10 +13,20 @@ config: type: string host: type: string + certificateIssuer: + type: string + default: acme acmeEmail: type: string + default: "" storageClass: type: string + apiReplicas: + type: integer + default: 2 + dbInstances: + type: integer + default: 2 resources: markupp: @@ -25,9 +35,12 @@ resources: namespace: ${namespace} image: ${image} host: ${host} + certificateIssuer: ${certificateIssuer} acmeEmail: ${acmeEmail} + apiReplicas: ${apiReplicas} database: storageClass: ${storageClass} + instances: ${dbInstances} outputs: url: ${markupp.url} From 01a863b553a50b37754e98533a1736e3f702a4f7 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 20:58:26 -0300 Subject: [PATCH 19/47] feat: adiciona as stacks dev e prod do pulumi --- deploy/stacks/Pulumi.dev.yaml | 7 +++++++ deploy/stacks/Pulumi.prod.yaml | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 deploy/stacks/Pulumi.dev.yaml create mode 100644 deploy/stacks/Pulumi.prod.yaml diff --git a/deploy/stacks/Pulumi.dev.yaml b/deploy/stacks/Pulumi.dev.yaml new file mode 100644 index 0000000..b0a98b3 --- /dev/null +++ b/deploy/stacks/Pulumi.dev.yaml @@ -0,0 +1,7 @@ +# Cluster kind efêmero do CI, que testa a implantação a cada push em dev. +config: + markupp:host: markupp.local + markupp:certificateIssuer: selfSigned + markupp:storageClass: standard + markupp:apiReplicas: 1 + markupp:dbInstances: 1 diff --git a/deploy/stacks/Pulumi.prod.yaml b/deploy/stacks/Pulumi.prod.yaml new file mode 100644 index 0000000..b4acf61 --- /dev/null +++ b/deploy/stacks/Pulumi.prod.yaml @@ -0,0 +1,4 @@ +# Enterprise no cluster do campus São José do IFSC. Domínio e email ACME +# entram aqui quando forem escolhidos, com pulumi config set. +config: + markupp:storageClass: longhorn-single From 8c7873ba1eddca53a16af3480789af8c3eeb9b95 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 21:18:13 -0300 Subject: [PATCH 20/47] feat: adiciona o cluster kind da stack dev com os requisitos de producao --- deploy/kind/cilium-values.yaml | 22 ++++++++++++++++ deploy/kind/cluster.yaml | 12 +++++++++ deploy/kind/prepara-cluster.sh | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 deploy/kind/cilium-values.yaml create mode 100644 deploy/kind/cluster.yaml create mode 100755 deploy/kind/prepara-cluster.sh diff --git a/deploy/kind/cilium-values.yaml b/deploy/kind/cilium-values.yaml new file mode 100644 index 0000000..260855f --- /dev/null +++ b/deploy/kind/cilium-values.yaml @@ -0,0 +1,22 @@ +# Espelha o cilium.yaml do cluster do IFSC: 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..d9c5895 --- /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 do IFSC. +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..454b31b --- /dev/null +++ b/deploy/kind/prepara-cluster.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Sobe o cluster kind da stack dev com os pré-requisitos que o cluster do IFSC +# tem: Gateway API, Cilium, cert-manager e CloudNativePG. +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 +CNPG_VERSION=1.30.1 + +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_operadores() { + kubectl apply -f \ + "https://github.com/cert-manager/cert-manager/releases/download/$CERT_MANAGER_VERSION/cert-manager.yaml" + kubectl apply --server-side -f \ + "https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v$CNPG_VERSION/cnpg-$CNPG_VERSION.yaml" + liga_gateway_no_cert_manager + kubectl wait --for=condition=Available --timeout=10m deployment --all -n cert-manager + kubectl wait --for=condition=Available --timeout=10m deployment --all -n cnpg-system +} + +# liga_gateway_no_cert_manager faz o cert-manager emitir certificado a partir +# da anotação do Gateway, como o controller do IFSC 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_operadores +kubectl create namespace "$NAMESPACE" From c1a70ea4a8fccb1fc76d50c4fbdfcf2d4c59f682 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 21:18:42 -0300 Subject: [PATCH 21/47] feat: testa a implantacao da stack dev num kind a cada push em dev --- .github/workflows/deploy-dev.yml | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/deploy-dev.yml diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..4645a00 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,68 @@ +name: Stack dev + +# Testa a implantação a cada push em dev: sobe um kind com os requisitos do +# cluster do IFSC, aplica a stack dev do Pulumi e bate no healthz por https. +# O cluster e o estado do Pulumi somem com o runner. + +on: + push: + branches: [dev] + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy-kind: + name: Implanta a stack dev num kind + runs-on: ubuntu-latest + env: + PULUMI_CONFIG_PASSPHRASE: "" + PULUMI_BACKEND_URL: file://${{ github.workspace }}/.pulumi-state + 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: Constroi a imagem do servidor + run: docker build -t markupp:dev markupp + + - name: Sobe o cluster com os requisitos + run: ./deploy/kind/prepara-cluster.sh + + - name: Carrega a imagem no cluster + run: kind load docker-image markupp:dev --name markupp-dev + + - name: Aplica a stack dev + working-directory: deploy/stacks + run: | + mkdir -p "${PULUMI_BACKEND_URL#file://}" + pulumi stack select dev --create --non-interactive + pulumi config set image markupp:dev + pulumi up --yes --non-interactive + + - name: Healthz por https pelo Gateway + run: | + for tentativa in $(seq 1 30); do + if curl -sfk --resolve markupp.local:8443:127.0.0.1 https://markupp.local:8443/healthz; then + exit 0 + fi + sleep 5 + done + kubectl -n markupp get all,certificate,gateway,httproute + exit 1 From 15bcae49a5221a75fa3c29c7321bacccdaef2bec Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 21:18:51 -0300 Subject: [PATCH 22/47] docs: descreve as stacks dev e prod no deploy --- docs/DEPLOY.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 1eeb9ae..ba3fea3 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -47,8 +47,26 @@ resources: storageClass: longhorn-single ``` -A stack da equipe, no cluster do IFSC, está em `deploy/ifsc` e usa o componente pelo caminho -local. +### Stacks da equipe + +O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stacks: + +- `prod`: o Enterprise no cluster do IFSC, com Let's Encrypt +- `dev`: um cluster kind que o CI sobe a cada push em dev para testar a implantação, com + certificado autoassinado + +Para rodar a stack dev na própria máquina, com kind, helm e Pulumi instalados: + +```sh +./deploy/kind/prepara-cluster.sh +docker build -t markupp:dev markupp +kind load docker-image markupp:dev --name markupp-dev +cd deploy/stacks +pulumi login --local +pulumi stack select dev --create +pulumi up --config image=markupp:dev +curl -k --resolve markupp.local:8443:127.0.0.1 https://markupp.local:8443/healthz +``` ## Desenvolvimento local From 192d9a9e2414c938ceba929f6092adee0a853d29 Mon Sep 17 00:00:00 2001 From: thnbi Date: Mon, 28 Sep 2026 21:57:37 -0300 Subject: [PATCH 23/47] feat: aponta a stack prod para markupp.negoci.online --- deploy/stacks/Pulumi.prod.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deploy/stacks/Pulumi.prod.yaml b/deploy/stacks/Pulumi.prod.yaml index b4acf61..2577d74 100644 --- a/deploy/stacks/Pulumi.prod.yaml +++ b/deploy/stacks/Pulumi.prod.yaml @@ -1,4 +1,5 @@ -# Enterprise no cluster do campus São José do IFSC. Domínio e email ACME -# entram aqui quando forem escolhidos, com pulumi config set. +# Enterprise no cluster do campus São José do IFSC. O email ACME entra aqui +# quando for escolhido, com pulumi config set. config: + markupp:host: markupp.negoci.online markupp:storageClass: longhorn-single From 3ae3f7e71b46e18d9e8d7b8df46da4e0a5c42f87 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:01:43 -0300 Subject: [PATCH 24/47] chore: roda o teste da stack dev em cada pr --- .github/workflows/deploy-dev.yml | 3 ++- docs/DEPLOY.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 4645a00..887dfea 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -1,10 +1,11 @@ name: Stack dev -# Testa a implantação a cada push em dev: sobe um kind com os requisitos do +# Testa a implantação em cada PR e a cada push em dev: sobe um kind com os requisitos do # cluster do IFSC, aplica a stack dev do Pulumi e bate no healthz por https. # O cluster e o estado do Pulumi somem com o runner. on: + pull_request: push: branches: [dev] workflow_dispatch: diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index ba3fea3..496cb5e 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -52,7 +52,7 @@ resources: O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stacks: - `prod`: o Enterprise no cluster do IFSC, com Let's Encrypt -- `dev`: um cluster kind que o CI sobe a cada push em dev para testar a implantação, com +- `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 a stack dev na própria máquina, com kind, helm e Pulumi instalados: From 5d9c00b6d474b7ef1b9a61b403efbf821849fcd6 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:07:37 -0300 Subject: [PATCH 25/47] test: espera o postgres no namespace no lugar do cloudnativepg --- deploy/pulumi/markupp_test.go | 68 +++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/deploy/pulumi/markupp_test.go b/deploy/pulumi/markupp_test.go index 56115fc..16e2929 100644 --- a/deploy/pulumi/markupp_test.go +++ b/deploy/pulumi/markupp_test.go @@ -10,8 +10,14 @@ import ( "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. +// 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 @@ -21,7 +27,11 @@ func (f *fakeResourceMonitor) NewResource(args pulumi.MockResourceArgs) (string, f.mu.Lock() defer f.mu.Unlock() f.resources = append(f.resources, args) - return args.Name + "-id", args.Inputs, nil + 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) { @@ -53,7 +63,7 @@ func argsDeTeste() MarkuppArgs { Image: "ghcr.io/markupp-labs/markupp:v1.1.0", Host: "markupp.dev.br", AcmeEmail: "equipe@markupp.dev.br", - Database: DatabaseArgs{StorageClass: "longhorn-single"}, + Database: DatabaseArgs{StorageClass: "longhorn-fast"}, } } @@ -88,6 +98,13 @@ func at(t *testing.T, pm resource.PropertyMap, keys ...string) resource.Property 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() @@ -105,22 +122,51 @@ func TestNewMarkupp_SemNamespace_RetornaErroComOCampo(t *testing.T) { assert.Contains(t, err.Error(), "namespace") } -func TestNewMarkupp_SemBancoExterno_CriaClusterCloudNativePG(t *testing.T) { +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-alpine", firstContainer(t, db)["image"].StringValue()) + 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()) - cluster := monitor.single(t, "kubernetes:postgresql.cnpg.io/v1:Cluster") - assert.Equal(t, 2.0, at(t, cluster, "spec", "instances").NumberValue()) - assert.Equal(t, "longhorn-single", at(t, cluster, "spec", "storage", "storageClass").StringValue()) - assert.Equal(t, "markupp", at(t, cluster, "metadata", "namespace").StringValue()) + 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_NaoCriaClusterEGuardaURLEmSecret(t *testing.T) { +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:postgresql.cnpg.io/v1:Cluster")) + 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()) @@ -135,7 +181,7 @@ func TestNewMarkupp_JobDeMigracao_RodaOSubcomandoMigrateNaImagem(t *testing.T) { assert.Equal(t, "migrate", container["args"].ArrayValue()[0].StringValue()) } -func TestNewMarkupp_DeploymentDaAPI_LeOBancoDoSecretDoCluster(t *testing.T) { +func TestNewMarkupp_DeploymentDaAPI_LeOBancoDoSecret(t *testing.T) { monitor := deploy(t, argsDeTeste()) api := monitor.single(t, "kubernetes:apps/v1:Deployment") From 553b59cd5af92c8081addac3511379834b07667a Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:09:09 -0300 Subject: [PATCH 26/47] feat: troca o cloudnativepg pelo postgres no namespace no componente --- deploy/pulumi/database.go | 143 ++++++++++++++++++++++++++++++-------- deploy/pulumi/go.mod | 1 + deploy/pulumi/go.sum | 2 + deploy/pulumi/markupp.go | 17 ++--- deploy/pulumi/workload.go | 2 +- 5 files changed, 125 insertions(+), 40 deletions(-) diff --git a/deploy/pulumi/database.go b/deploy/pulumi/database.go index 2ce3d92..37d2048 100644 --- a/deploy/pulumi/database.go +++ b/deploy/pulumi/database.go @@ -1,25 +1,31 @@ package main import ( - "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes" - "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" + 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" ) -// dbSecretKey é a chave com a URL de conexão. O CloudNativePG já grava a URL -// com esse nome no Secret -app, e o banco externo segue o mesmo -// formato para a API ler de um lugar só. -const dbSecretKey = "uri" +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-alpine" + postgresPort = 5432 + postgresUser = "markupp" + passwordLength = 32 + postgresDataPath = "/var/lib/postgresql/data" +) -// declareDatabase cria o banco ou o Secret do banco externo, e devolve o -// recurso de que a migração depende. +// 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 declareCloudNativePG(ctx, names, args, parent) + return declareNamespacePostgres(ctx, names, args, parent) } // declareExternalDatabase guarda a URL num Secret. O provider Kubernetes já @@ -31,31 +37,112 @@ func declareExternalDatabase(ctx *pulumi.Context, names resourceNames, args Mark }, parent) } -func declareCloudNativePG(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) (pulumi.Resource, error) { - storage := pulumi.Map{"size": pulumi.String(args.Database.Size)} - if args.Database.StorageClass != "" { - storage["storageClass"] = pulumi.String(args.Database.StorageClass) - } - return apiextensions.NewCustomResource(ctx, names.database, &apiextensions.CustomResourceArgs{ - ApiVersion: pulumi.String("postgresql.cnpg.io/v1"), - Kind: pulumi.String("Cluster"), - Metadata: namespaced(names.database, args.Namespace), - OtherFields: kubernetes.UntypedArgs{"spec": pulumi.Map{ - "instances": pulumi.Int(args.Database.Instances), - "storage": storage, - "bootstrap": pulumi.Map{"initdb": pulumi.Map{"database": pulumi.String("markupp"), "owner": pulumi.String("markupp")}}, - }}, +// 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{corev1.EnvVarArgs{ - Name: pulumi.String("MARKUPP_DATABASE_URL"), + 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(names.dbSecret), - Key: pulumi.String(dbSecretKey), + Name: pulumi.String(secret), + Key: pulumi.String(key), }}, - }} + } } func namespaced(name, namespace string) metav1.ObjectMetaArgs { diff --git a/deploy/pulumi/go.mod b/deploy/pulumi/go.mod index d97fa4e..a3822bd 100644 --- a/deploy/pulumi/go.mod +++ b/deploy/pulumi/go.mod @@ -5,6 +5,7 @@ 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 ) diff --git a/deploy/pulumi/go.sum b/deploy/pulumi/go.sum index 8035afe..4e16bc9 100644 --- a/deploy/pulumi/go.sum +++ b/deploy/pulumi/go.sum @@ -186,6 +186,8 @@ github.com/pulumi/pulumi-go-provider v1.7.0 h1:wrkDxqcHQTO3CjAjDqjBfYxQCMUNaYLbK 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= diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go index 7769e8b..dd82ed9 100644 --- a/deploy/pulumi/markupp.go +++ b/deploy/pulumi/markupp.go @@ -12,7 +12,6 @@ const ( issuerACME = "acme" issuerSelfSigned = "selfSigned" defaultAPIReplicas = 2 - defaultDBInstances = 2 defaultDBSize = "10Gi" apiPort = 8080 ) @@ -47,16 +46,15 @@ type MarkuppArgs struct { Database DatabaseArgs `pulumi:"database"` } -// DatabaseArgs escolhe entre um PostgreSQL externo e um cluster CloudNativePG. +// 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 cria um - // cluster CloudNativePG, que exige o operador instalado. + // 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"` - // Instances é o número de instâncias do CloudNativePG. Padrão 2. - Instances int `pulumi:"instances,optional"` - // StorageClass dos volumes do CloudNativePG. + // StorageClass do volume do PostgreSQL do namespace. StorageClass string `pulumi:"storageClass,optional"` - // Size de cada volume do CloudNativePG. Padrão 10Gi. + // Size do volume do PostgreSQL do namespace. Padrão 10Gi. Size string `pulumi:"size,optional"` } @@ -123,9 +121,6 @@ func withDefaults(args MarkuppArgs) MarkuppArgs { if args.APIReplicas == 0 { args.APIReplicas = defaultAPIReplicas } - if args.Database.Instances == 0 { - args.Database.Instances = defaultDBInstances - } if args.Database.Size == "" { args.Database.Size = defaultDBSize } diff --git a/deploy/pulumi/workload.go b/deploy/pulumi/workload.go index 8207149..cb3758a 100644 --- a/deploy/pulumi/workload.go +++ b/deploy/pulumi/workload.go @@ -8,7 +8,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) -// migrationBackoff cobre o tempo de o CloudNativePG aceitar conexão na primeira +// 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 From b01af170d653d194d572717a4ee66d15268c8af6 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:09:23 -0300 Subject: [PATCH 27/47] refact: tira o numero de instancias do banco das stacks --- deploy/stacks/Pulumi.dev.yaml | 1 - deploy/stacks/Pulumi.yaml | 4 ---- 2 files changed, 5 deletions(-) diff --git a/deploy/stacks/Pulumi.dev.yaml b/deploy/stacks/Pulumi.dev.yaml index b0a98b3..043b6e2 100644 --- a/deploy/stacks/Pulumi.dev.yaml +++ b/deploy/stacks/Pulumi.dev.yaml @@ -4,4 +4,3 @@ config: markupp:certificateIssuer: selfSigned markupp:storageClass: standard markupp:apiReplicas: 1 - markupp:dbInstances: 1 diff --git a/deploy/stacks/Pulumi.yaml b/deploy/stacks/Pulumi.yaml index faefe2d..f87975a 100644 --- a/deploy/stacks/Pulumi.yaml +++ b/deploy/stacks/Pulumi.yaml @@ -24,9 +24,6 @@ config: apiReplicas: type: integer default: 2 - dbInstances: - type: integer - default: 2 resources: markupp: @@ -40,7 +37,6 @@ resources: apiReplicas: ${apiReplicas} database: storageClass: ${storageClass} - instances: ${dbInstances} outputs: url: ${markupp.url} From 5792e2d93d7c5727a53300d3036ea4c874cd4ac8 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:09:24 -0300 Subject: [PATCH 28/47] feat: usa a storageclass que o cluster de producao ja tem na stack prod --- deploy/stacks/Pulumi.prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/stacks/Pulumi.prod.yaml b/deploy/stacks/Pulumi.prod.yaml index 2577d74..931bbb8 100644 --- a/deploy/stacks/Pulumi.prod.yaml +++ b/deploy/stacks/Pulumi.prod.yaml @@ -2,4 +2,4 @@ # quando for escolhido, com pulumi config set. config: markupp:host: markupp.negoci.online - markupp:storageClass: longhorn-single + markupp:storageClass: longhorn-fast From 524c7d25c010f60fc771899d3acdda61a5623552 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:10:08 -0300 Subject: [PATCH 29/47] docs: troca o cloudnativepg pelo postgres no namespace --- docs/DEPLOY.md | 12 ++++++------ docs/adrs/ADR-0025-iac-com-pulumi.md | 7 +++---- docs/adrs/ADR-0027-kubernetes-como-plataforma.md | 6 +++++- docs/diagramas-de-blocos.md | 6 +++--- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 496cb5e..2c87700 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -10,7 +10,8 @@ tag `v*`, e a configuração vem de variáveis de ambiente, descritas no A instalação em Kubernetes é o componente Pulumi em `deploy/pulumi` (ADR-0025). Ele cria, no namespace informado: -- o banco: um cluster CloudNativePG, ou um Secret com a URL de um PostgreSQL externo; +- 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. @@ -20,10 +21,9 @@ namespace informado: - 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, Service, Gateway, - HTTPRoute, Issuer e, com CloudNativePG, Cluster -- opcional: o operador CloudNativePG e uma StorageClass para os volumes dele. Sem ele, informe - `database.externalUrl` +- 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 @@ -44,7 +44,7 @@ resources: host: notas.exemplo.com acmeEmail: ops@exemplo.com database: - storageClass: longhorn-single + storageClass: longhorn-fast ``` ### Stacks da equipe diff --git a/docs/adrs/ADR-0025-iac-com-pulumi.md b/docs/adrs/ADR-0025-iac-com-pulumi.md index 842ce18..b07dda1 100644 --- a/docs/adrs/ADR-0025-iac-com-pulumi.md +++ b/docs/adrs/ADR-0025-iac-com-pulumi.md @@ -19,8 +19,7 @@ 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 e CloudNativePG são opcionais, instalados à parte por quem -administra o cluster +pré-requisitos do cluster, e KEDA é opcional, instalado à parte por quem administra o cluster ## Alternativas consideradas @@ -44,6 +43,6 @@ administra o cluster 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 -- Sem KEDA, os indexadores rodam por CronJob. Sem CloudNativePG, o componente recebe a conexão de - um Postgres qualquer +- 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 f8d3e2a..957d70b 100644 --- a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md +++ b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md @@ -19,7 +19,8 @@ mesmas imagens de container usadas no compose. A indexação roda como Job criad A implantação vem em duas fases. Na primeira, o Enterprise roda no cluster kubeadm do campus São José do IFSC, com Cilium, Gateway API, cert-manager e Longhorn. Na segunda, vai para nuvem -pública, com a mesma instalação. O PostgreSQL roda dentro do cluster, com CloudNativePG +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 @@ -28,6 +29,8 @@ pública, com a mesma instalação. O PostgreSQL roda dentro do cluster, com Clo 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 @@ -44,6 +47,7 @@ pública, com a mesma instalação. O PostgreSQL roda dentro do cluster, com Clo 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/diagramas-de-blocos.md b/docs/diagramas-de-blocos.md index e782d17..6ed6c9f 100644 --- a/docs/diagramas-de-blocos.md +++ b/docs/diagramas-de-blocos.md @@ -54,9 +54,9 @@ 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 CloudNativePG em duas instâncias, 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. O backup é snapshot de volume no próprio cluster, então perder o cluster leva as notas +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 From 9c8fbba8c8121eb36b2f0dfe0bdb717d216fefbd Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:17:03 -0300 Subject: [PATCH 30/47] chore: tira o cloudnativepg do cluster kind da stack dev --- deploy/kind/prepara-cluster.sh | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/deploy/kind/prepara-cluster.sh b/deploy/kind/prepara-cluster.sh index 454b31b..9a155de 100755 --- a/deploy/kind/prepara-cluster.sh +++ b/deploy/kind/prepara-cluster.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Sobe o cluster kind da stack dev com os pré-requisitos que o cluster do IFSC -# tem: Gateway API, Cilium, cert-manager e CloudNativePG. +# tem: Gateway API, Cilium e cert-manager. set -euo pipefail CLUSTER="${CLUSTER:-markupp-dev}" @@ -9,7 +9,6 @@ DIR="$(cd "$(dirname "$0")" && pwd)" GATEWAY_API_VERSION=v1.6.1 CILIUM_VERSION=1.20.2 CERT_MANAGER_VERSION=v1.21.2 -CNPG_VERSION=1.30.1 cria_cluster() { kind create cluster --name "$CLUSTER" --config "$DIR/cluster.yaml" @@ -23,14 +22,11 @@ instala_cilium() { --set k8sServiceHost="$CLUSTER-control-plane" --wait --timeout 10m } -instala_operadores() { +instala_cert_manager() { kubectl apply -f \ "https://github.com/cert-manager/cert-manager/releases/download/$CERT_MANAGER_VERSION/cert-manager.yaml" - kubectl apply --server-side -f \ - "https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v$CNPG_VERSION/cnpg-$CNPG_VERSION.yaml" liga_gateway_no_cert_manager kubectl wait --for=condition=Available --timeout=10m deployment --all -n cert-manager - kubectl wait --for=condition=Available --timeout=10m deployment --all -n cnpg-system } # liga_gateway_no_cert_manager faz o cert-manager emitir certificado a partir @@ -42,5 +38,5 @@ liga_gateway_no_cert_manager() { cria_cluster instala_cilium -instala_operadores +instala_cert_manager kubectl create namespace "$NAMESPACE" From 37a23d421d720e9bfea5068df99d224d31db0b8d Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:31:18 -0300 Subject: [PATCH 31/47] feat: adiciona o smoke descartavel da stack dev num kind --- deploy/kind/smoke.sh | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100755 deploy/kind/smoke.sh diff --git a/deploy/kind/smoke.sh b/deploy/kind/smoke.sh new file mode 100755 index 0000000..8526d50 --- /dev/null +++ b/deploy/kind/smoke.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Smoke da stack dev: sobe um kind com os requisitos do cluster do IFSC, aplica +# a stack dev do Pulumi, testa a API por https pelo Gateway e apaga tudo no fim, +# passe ou falhe. O CI roda este mesmo script. +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +STACKS="$REPO/deploy/stacks" +IMAGE=markupp:dev +URL=https://markupp.local:8443 +WORKDIR="$(mktemp -d)" + +export CLUSTER=markupp-dev +export KUBECONFIG="$WORKDIR/kubeconfig" +export PULUMI_BACKEND_URL="file://$WORKDIR/state" +export PULUMI_CONFIG_PASSPHRASE="" + +limpa() { + kind delete cluster --name "$CLUSTER" >/dev/null 2>&1 || true + cp "$WORKDIR/Pulumi.dev.yaml" "$STACKS/Pulumi.dev.yaml" 2>/dev/null || true + rm -rf "$WORKDIR" +} + +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() { + mkdir -p "$WORKDIR/state" + cp "$STACKS/Pulumi.dev.yaml" "$WORKDIR/Pulumi.dev.yaml" + cd "$STACKS" + pulumi stack select dev --create --non-interactive + pulumi config set image "$IMAGE" + pulumi up --yes --non-interactive +} + +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 +} + +trap limpa EXIT +sobe_cluster +aplica_stack +testa_api +echo "smoke passou" From 8cb1c88661a64ed2cbf5f78a118a4e41928dbedf Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:31:18 -0300 Subject: [PATCH 32/47] chore: expoe o smoke da stack dev no make smoke-kind --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 92e7553..d29d911 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 compose-config compose-env docker-up docker-down run hooks test-scripts coverage coverage-check all: compose-env compose-config test @@ -12,6 +12,11 @@ all: compose-env compose-config test test: cd markupp && go test ./... +# Smoke descartável da stack dev num kind com os requisitos do cluster do IFSC. +# Precisa de Docker, kind, helm, kubectl e Pulumi, e apaga o cluster no fim +smoke-kind: + ./deploy/kind/smoke.sh + # Valida o arquivo docker-compose e a interpolação de variáveis de ambiente compose-config: $(DOCKER_COMPOSE) -f $(COMPOSE_FILE) config From a6b039afe7a4e584633d67f7bfb7cdc5b8723fc3 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:31:19 -0300 Subject: [PATCH 33/47] refact: faz o ci da stack dev rodar o mesmo script do smoke local --- .github/workflows/deploy-dev.yml | 38 +++++--------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 887dfea..dc13ffd 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -1,8 +1,8 @@ name: Stack dev -# Testa a implantação em cada PR e a cada push em dev: sobe um kind com os requisitos do -# cluster do IFSC, aplica a stack dev do Pulumi e bate no healthz por https. -# O cluster e o estado do Pulumi somem com o runner. +# 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 do IFSC, stack dev do +# Pulumi e API testada por https pelo Gateway. on: pull_request: @@ -17,9 +17,6 @@ jobs: deploy-kind: name: Implanta a stack dev num kind runs-on: ubuntu-latest - env: - PULUMI_CONFIG_PASSPHRASE: "" - PULUMI_BACKEND_URL: file://${{ github.workspace }}/.pulumi-state steps: - uses: actions/checkout@v4 @@ -40,30 +37,5 @@ jobs: - name: Instala o Pulumi uses: pulumi/actions@v7 - - name: Constroi a imagem do servidor - run: docker build -t markupp:dev markupp - - - name: Sobe o cluster com os requisitos - run: ./deploy/kind/prepara-cluster.sh - - - name: Carrega a imagem no cluster - run: kind load docker-image markupp:dev --name markupp-dev - - - name: Aplica a stack dev - working-directory: deploy/stacks - run: | - mkdir -p "${PULUMI_BACKEND_URL#file://}" - pulumi stack select dev --create --non-interactive - pulumi config set image markupp:dev - pulumi up --yes --non-interactive - - - name: Healthz por https pelo Gateway - run: | - for tentativa in $(seq 1 30); do - if curl -sfk --resolve markupp.local:8443:127.0.0.1 https://markupp.local:8443/healthz; then - exit 0 - fi - sleep 5 - done - kubectl -n markupp get all,certificate,gateway,httproute - exit 1 + - name: Smoke da stack dev num kind + run: ./deploy/kind/smoke.sh From 69d1e01e065d3217df909e7f6e7e0ea933d7da34 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:31:19 -0300 Subject: [PATCH 34/47] docs: troca os passos da stack dev local pelo make smoke-kind --- docs/DEPLOY.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 2c87700..65f36ec 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -55,19 +55,16 @@ O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stac - `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 a stack dev na própria máquina, com kind, helm e Pulumi instalados: +Para rodar o mesmo teste da stack dev na própria máquina, com Docker, kind, helm, kubectl e +Pulumi instalados: ```sh -./deploy/kind/prepara-cluster.sh -docker build -t markupp:dev markupp -kind load docker-image markupp:dev --name markupp-dev -cd deploy/stacks -pulumi login --local -pulumi stack select dev --create -pulumi up --config image=markupp:dev -curl -k --resolve markupp.local:8443:127.0.0.1 https://markupp.local:8443/healthz +make smoke-kind ``` +O smoke usa kubeconfig e estado do Pulumi próprios, num diretório temporário, e apaga o +cluster no fim, passe ou falhe. + ## Desenvolvimento local O compose sobe PostgreSQL, aplica as migrações e roda o servidor com recarga automática: From 5f1ee9fb96c8585e9cf1ac53033400a7f4997c3d Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:34:05 -0300 Subject: [PATCH 35/47] refact: fixa a imagem local e o salt na stack dev --- deploy/kind/smoke.sh | 3 --- deploy/stacks/Pulumi.dev.yaml | 8 ++++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/deploy/kind/smoke.sh b/deploy/kind/smoke.sh index 8526d50..4f4c27b 100755 --- a/deploy/kind/smoke.sh +++ b/deploy/kind/smoke.sh @@ -17,7 +17,6 @@ export PULUMI_CONFIG_PASSPHRASE="" limpa() { kind delete cluster --name "$CLUSTER" >/dev/null 2>&1 || true - cp "$WORKDIR/Pulumi.dev.yaml" "$STACKS/Pulumi.dev.yaml" 2>/dev/null || true rm -rf "$WORKDIR" } @@ -29,10 +28,8 @@ sobe_cluster() { aplica_stack() { mkdir -p "$WORKDIR/state" - cp "$STACKS/Pulumi.dev.yaml" "$WORKDIR/Pulumi.dev.yaml" cd "$STACKS" pulumi stack select dev --create --non-interactive - pulumi config set image "$IMAGE" pulumi up --yes --non-interactive } diff --git a/deploy/stacks/Pulumi.dev.yaml b/deploy/stacks/Pulumi.dev.yaml index 043b6e2..0d3f52a 100644 --- a/deploy/stacks/Pulumi.dev.yaml +++ b/deploy/stacks/Pulumi.dev.yaml @@ -1,6 +1,10 @@ -# Cluster kind efêmero do CI, que testa a implantação a cada push em dev. +# 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:apiReplicas: "1" + markupp:image: markupp:dev +encryptionsalt: v1:tbTefk4lOTE=:v1:LIkxKMN7jxMuArZU:7rDPXfPSUFiFmPLrl91RUTCRM4s6zA== From a1e2695fd22c082e76eaf0de2c3f1b0c78da20a9 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:38:42 -0300 Subject: [PATCH 36/47] feat: adiciona o ambiente kind de pe com o painel headlamp --- .github/workflows/deploy-dev.yml | 2 +- Makefile | 16 ++++- deploy/kind/ambiente.sh | 113 +++++++++++++++++++++++++++++++ deploy/kind/smoke.sh | 58 ---------------- 4 files changed, 128 insertions(+), 61 deletions(-) create mode 100755 deploy/kind/ambiente.sh delete mode 100755 deploy/kind/smoke.sh diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index dc13ffd..5f8dd32 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -38,4 +38,4 @@ jobs: uses: pulumi/actions@v7 - name: Smoke da stack dev num kind - run: ./deploy/kind/smoke.sh + run: ./deploy/kind/ambiente.sh smoke diff --git a/Makefile b/Makefile index d29d911..523d0e3 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ DOCKER_COMPOSE=docker compose META_COVERAGE=90 PERFIL_COVERAGE=markupp/cover.out -.PHONY: all test smoke-kind 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 @@ -15,7 +15,19 @@ test: # Smoke descartável da stack dev num kind com os requisitos do cluster do IFSC. # Precisa de Docker, kind, helm, kubectl e Pulumi, e apaga o cluster no fim smoke-kind: - ./deploy/kind/smoke.sh + ./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: diff --git a/deploy/kind/ambiente.sh b/deploy/kind/ambiente.sh new file mode 100755 index 0000000..e800d10 --- /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 do IFSC. +# +# 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/smoke.sh b/deploy/kind/smoke.sh deleted file mode 100755 index 4f4c27b..0000000 --- a/deploy/kind/smoke.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# Smoke da stack dev: sobe um kind com os requisitos do cluster do IFSC, aplica -# a stack dev do Pulumi, testa a API por https pelo Gateway e apaga tudo no fim, -# passe ou falhe. O CI roda este mesmo script. -set -euo pipefail - -REPO="$(cd "$(dirname "$0")/../.." && pwd)" -STACKS="$REPO/deploy/stacks" -IMAGE=markupp:dev -URL=https://markupp.local:8443 -WORKDIR="$(mktemp -d)" - -export CLUSTER=markupp-dev -export KUBECONFIG="$WORKDIR/kubeconfig" -export PULUMI_BACKEND_URL="file://$WORKDIR/state" -export PULUMI_CONFIG_PASSPHRASE="" - -limpa() { - kind delete cluster --name "$CLUSTER" >/dev/null 2>&1 || true - rm -rf "$WORKDIR" -} - -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() { - mkdir -p "$WORKDIR/state" - cd "$STACKS" - pulumi stack select dev --create --non-interactive - pulumi up --yes --non-interactive -} - -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 -} - -trap limpa EXIT -sobe_cluster -aplica_stack -testa_api -echo "smoke passou" From 53d1b5d21353e41de8cc27af996feae52aa4552b Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:38:42 -0300 Subject: [PATCH 37/47] docs: descreve o ambiente kind de pe com o painel --- docs/DEPLOY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 65f36ec..92413f0 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -65,6 +65,17 @@ make smoke-kind O smoke usa kubeconfig e estado do Pulumi próprios, num diretório temporário, e apaga o cluster no fim, passe ou falhe. +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: From 92da34d530677b8b669a6f765c5ebfc458b29de8 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:50:28 -0300 Subject: [PATCH 38/47] docs: neutraliza o local do cluster de producao nos textos --- .github/workflows/deploy-dev.yml | 2 +- Makefile | 2 +- deploy/kind/ambiente.sh | 2 +- deploy/kind/cilium-values.yaml | 2 +- deploy/kind/cluster.yaml | 2 +- deploy/kind/prepara-cluster.sh | 4 ++-- deploy/stacks/Pulumi.prod.yaml | 2 +- docs/DEPLOY.md | 2 +- docs/adrs/ADR-0025-iac-com-pulumi.md | 2 +- docs/adrs/ADR-0027-kubernetes-como-plataforma.md | 8 ++++---- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 5f8dd32..5fdb135 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -1,7 +1,7 @@ name: 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 do IFSC, stack dev do +# make smoke-kind: kind com os requisitos do cluster de produção, stack dev do # Pulumi e API testada por https pelo Gateway. on: diff --git a/Makefile b/Makefile index 523d0e3..a51f91e 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ all: compose-env compose-config test test: cd markupp && go test ./... -# Smoke descartável da stack dev num kind com os requisitos do cluster do IFSC. +# 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 diff --git a/deploy/kind/ambiente.sh b/deploy/kind/ambiente.sh index e800d10..579ce7d 100755 --- a/deploy/kind/ambiente.sh +++ b/deploy/kind/ambiente.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Ambiente da stack dev num kind com os requisitos do cluster do IFSC. +# 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 diff --git a/deploy/kind/cilium-values.yaml b/deploy/kind/cilium-values.yaml index 260855f..065216f 100644 --- a/deploy/kind/cilium-values.yaml +++ b/deploy/kind/cilium-values.yaml @@ -1,4 +1,4 @@ -# Espelha o cilium.yaml do cluster do IFSC: Gateway API com o Envoy na rede +# 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 diff --git a/deploy/kind/cluster.yaml b/deploy/kind/cluster.yaml index d9c5895..773fb48 100644 --- a/deploy/kind/cluster.yaml +++ b/deploy/kind/cluster.yaml @@ -1,5 +1,5 @@ # Cluster da stack dev. Sem CNI nem kube-proxy padrão, porque o Cilium -# substitui os dois, como no cluster do IFSC. +# substitui os dois, como no cluster de produção. kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 networking: diff --git a/deploy/kind/prepara-cluster.sh b/deploy/kind/prepara-cluster.sh index 9a155de..d9c8e52 100755 --- a/deploy/kind/prepara-cluster.sh +++ b/deploy/kind/prepara-cluster.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Sobe o cluster kind da stack dev com os pré-requisitos que o cluster do IFSC +# 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 @@ -30,7 +30,7 @@ instala_cert_manager() { } # liga_gateway_no_cert_manager faz o cert-manager emitir certificado a partir -# da anotação do Gateway, como o controller do IFSC roda com --enable-gateway-api. +# 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"}]' diff --git a/deploy/stacks/Pulumi.prod.yaml b/deploy/stacks/Pulumi.prod.yaml index 931bbb8..6c51d7b 100644 --- a/deploy/stacks/Pulumi.prod.yaml +++ b/deploy/stacks/Pulumi.prod.yaml @@ -1,4 +1,4 @@ -# Enterprise no cluster do campus São José do IFSC. O email ACME entra aqui +# Enterprise no cluster de produção. O email ACME entra aqui # quando for escolhido, com pulumi config set. config: markupp:host: markupp.negoci.online diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 92413f0..1994a10 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -51,7 +51,7 @@ resources: O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stacks: -- `prod`: o Enterprise no cluster do IFSC, com Let's Encrypt +- `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 diff --git a/docs/adrs/ADR-0025-iac-com-pulumi.md b/docs/adrs/ADR-0025-iac-com-pulumi.md index b07dda1..e1f2167 100644 --- a/docs/adrs/ADR-0025-iac-com-pulumi.md +++ b/docs/adrs/ADR-0025-iac-com-pulumi.md @@ -6,7 +6,7 @@ Aceita ## Contexto -O Enterprise roda em Kubernetes (ADR-0027), primeiro no cluster do IFSC e depois em nuvem +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 diff --git a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md index 957d70b..349bbb9 100644 --- a/docs/adrs/ADR-0027-kubernetes-como-plataforma.md +++ b/docs/adrs/ADR-0027-kubernetes-como-plataforma.md @@ -17,10 +17,10 @@ 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 no cluster kubeadm do campus -São José do IFSC, 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 +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 From 77c90518f0e29c73c7c15a5a98a0d89ffc63bf37 Mon Sep 17 00:00:00 2001 From: thnbi Date: Wed, 30 Sep 2026 21:51:43 -0300 Subject: [PATCH 39/47] docs: adiciona o diagrama de implantacao no kubernetes --- docs/diagrama-de-implantacao.drawio.svg | 4 +++ docs/diagrama-de-implantacao.md | 38 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 docs/diagrama-de-implantacao.drawio.svg create mode 100644 docs/diagrama-de-implantacao.md 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. From f72585c51129beffac19ccb4c99c8b6287f8ce2a Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:40:04 -0300 Subject: [PATCH 40/47] test: espera gateway existente, replicas zero e postgres fixo no componente --- deploy/pulumi/markupp_test.go | 60 ++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/deploy/pulumi/markupp_test.go b/deploy/pulumi/markupp_test.go index 16e2929..fccf290 100644 --- a/deploy/pulumi/markupp_test.go +++ b/deploy/pulumi/markupp_test.go @@ -128,7 +128,8 @@ func TestNewMarkupp_SemBancoExterno_CriaPostgresNoNamespaceComVolume(t *testing. 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-alpine", firstContainer(t, db)["image"].StringValue()) + 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()) @@ -242,6 +243,7 @@ func TestNewMarkupp_EmissorACMESemEmail_RetornaErroComOCampo(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "acmeEmail") + assert.Contains(t, err.Error(), "certificateIssuer") } func TestNewMarkupp_EmissorDesconhecido_RetornaErroComOValor(t *testing.T) { @@ -254,3 +256,59 @@ func TestNewMarkupp_EmissorDesconhecido_RetornaErroComOValor(t *testing.T) { 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") +} From 4fa879cf263236aac0dca0c93e8a9191e4df8596 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:40:50 -0300 Subject: [PATCH 41/47] feat: pendura o httproute num gateway existente no componente --- deploy/pulumi/ingress.go | 24 +++++++++++++++------ deploy/pulumi/markupp.go | 46 +++++++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/deploy/pulumi/ingress.go b/deploy/pulumi/ingress.go index fce4062..5a690a7 100644 --- a/deploy/pulumi/ingress.go +++ b/deploy/pulumi/ingress.go @@ -12,17 +12,21 @@ const ( httpsListener = "https" ) -// declareIngress cria Issuer, Gateway e HTTPRoute no namespace do markupp. -// O Issuer é do namespace, e não ClusterIssuer, para a credencial do CI não -// precisar de permissão no cluster inteiro. +// 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, parent) + return declareRoute(ctx, names, args, gatewayRef(names, args, httpsListener), parent) } func declareIssuer(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { @@ -73,10 +77,10 @@ func declareGateway(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, return err } -func declareRoute(ctx *pulumi.Context, names resourceNames, args MarkuppArgs, parent pulumi.ResourceOption) error { +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{gatewayRef(names, args, httpsListener)}, + "parentRefs": pulumi.Array{gateway}, "hostnames": pulumi.StringArray{pulumi.String(args.Host)}, "rules": pulumi.Array{pulumi.Map{"backendRefs": pulumi.Array{backend}}}, }, parent) @@ -90,6 +94,14 @@ func gatewayRef(names resourceNames, args MarkuppArgs, listener string) pulumi.M } } +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), diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go index dd82ed9..3b8ba68 100644 --- a/deploy/pulumi/markupp.go +++ b/deploy/pulumi/markupp.go @@ -40,12 +40,26 @@ type MarkuppArgs struct { 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. 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 { @@ -91,18 +105,15 @@ func declareResources(ctx *pulumi.Context, name string, args MarkuppArgs, parent return declareIngress(ctx, names, args, parent) } +type requiredField struct{ field, value string } + func validate(args MarkuppArgs) error { - required := []struct{ field, value string }{ - {"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}, - } - switch args.CertificateIssuer { - case "", issuerACME: - required = append(required, struct{ field, value string }{"acmeEmail", args.AcmeEmail}) - case issuerSelfSigned: - default: - return fmt.Errorf("certificateIssuer=%q, esperado %q ou %q", args.CertificateIssuer, issuerACME, issuerSelfSigned) + required := []requiredField{{"namespace", args.Namespace}, {"image", args.Image}, {"host", args.Host}} + ingress, err := ingressRequiredFields(args) + if err != nil { + return err } - for _, r := range required { + 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)", r.field, args.Namespace, args.Image, args.Host) @@ -111,6 +122,21 @@ func validate(args MarkuppArgs) error { 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 From cdbebae629845731a755a7557ab55d31e5cab290 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:05 -0300 Subject: [PATCH 42/47] fix: aceita zero replicas da api no componente --- deploy/pulumi/markupp.go | 10 ++++++---- deploy/pulumi/workload.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go index 3b8ba68..d64ff83 100644 --- a/deploy/pulumi/markupp.go +++ b/deploy/pulumi/markupp.go @@ -44,8 +44,9 @@ type MarkuppArgs struct { // 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. - APIReplicas int `pulumi:"apiReplicas,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"` } @@ -144,8 +145,9 @@ func withDefaults(args MarkuppArgs) MarkuppArgs { if args.GatewayClassName == "" { args.GatewayClassName = defaultGatewayClass } - if args.APIReplicas == 0 { - args.APIReplicas = defaultAPIReplicas + if args.APIReplicas == nil { + replicas := defaultAPIReplicas + args.APIReplicas = &replicas } if args.Database.Size == "" { args.Database.Size = defaultDBSize diff --git a/deploy/pulumi/workload.go b/deploy/pulumi/workload.go index cb3758a..dc57ac0 100644 --- a/deploy/pulumi/workload.go +++ b/deploy/pulumi/workload.go @@ -56,7 +56,7 @@ func apiDeploymentArgs(names resourceNames, args MarkuppArgs, labels pulumi.Stri return &appsv1.DeploymentArgs{ Metadata: namespaced(names.api, args.Namespace), Spec: appsv1.DeploymentSpecArgs{ - Replicas: pulumi.Int(args.APIReplicas), + Replicas: pulumi.Int(*args.APIReplicas), Selector: metav1.LabelSelectorArgs{MatchLabels: labels}, Template: corev1.PodTemplateSpecArgs{ Metadata: metav1.ObjectMetaArgs{Labels: labels}, From 3c41850dca714281ae4a59f34d71d88d1de15433 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:08 -0300 Subject: [PATCH 43/47] chore: fixa a versao do postgres no componente pulumi --- deploy/pulumi/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/pulumi/database.go b/deploy/pulumi/database.go index 37d2048..cd99af2 100644 --- a/deploy/pulumi/database.go +++ b/deploy/pulumi/database.go @@ -12,7 +12,7 @@ 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-alpine" + postgresImage = "postgres:17.9-alpine3.23" postgresPort = 5432 postgresUser = "markupp" passwordLength = 32 From 99c20c97a1be7cc7ca83706a9efefb48a53897f6 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:24 -0300 Subject: [PATCH 44/47] fix: traz emissor e email acme na mensagem de campo vazio do componente --- deploy/pulumi/markupp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/pulumi/markupp.go b/deploy/pulumi/markupp.go index d64ff83..fdcacfa 100644 --- a/deploy/pulumi/markupp.go +++ b/deploy/pulumi/markupp.go @@ -116,8 +116,8 @@ func validate(args MarkuppArgs) error { } 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)", - r.field, args.Namespace, args.Image, args.Host) + 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 From a043f002f5d4c6952cb1ea21eeb691c745f3b367 Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:35 -0300 Subject: [PATCH 45/47] refact: renomeia o workflow da stack dev para smoke --- .github/workflows/{deploy-dev.yml => smoke-dev.yml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{deploy-dev.yml => smoke-dev.yml} (92%) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/smoke-dev.yml similarity index 92% rename from .github/workflows/deploy-dev.yml rename to .github/workflows/smoke-dev.yml index 5fdb135..8da33d6 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/smoke-dev.yml @@ -1,4 +1,4 @@ -name: Stack dev +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 @@ -14,8 +14,8 @@ permissions: contents: read jobs: - deploy-kind: - name: Implanta a stack dev num kind + smoke-kind: + name: Smoke da stack dev num kind runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From c99f2cfc800af9040bd65051ab812041fc0f24cd Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:35 -0300 Subject: [PATCH 46/47] chore: roda o smoke da stack dev so quando muda servidor ou deploy --- .github/workflows/smoke-dev.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/smoke-dev.yml b/.github/workflows/smoke-dev.yml index 8da33d6..8c9562f 100644 --- a/.github/workflows/smoke-dev.yml +++ b/.github/workflows/smoke-dev.yml @@ -6,8 +6,16 @@ name: Smoke da stack dev 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: From f23d4f35a8e25d2305bea04f1b6af2c9b4ea63ab Mon Sep 17 00:00:00 2001 From: thnbi Date: Fri, 2 Oct 2026 19:41:44 -0300 Subject: [PATCH 47/47] docs: descreve o gateway existente no deploy --- docs/DEPLOY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 1994a10..20f9e28 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -47,6 +47,12 @@ resources: storageClass: longhorn-fast ``` +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. + ### Stacks da equipe O projeto em `deploy/stacks` usa o componente pelo caminho local e tem duas stacks: