Crie seu primeiro módulo NNumbers em Java
Neste tutorial você cria o módulo notes: um serviço que guarda itens
por organização e recarrega cada item em segundo plano. No caminho, você usa
tudo o que um módulo de verdade usa — rota, Action assíncrona, permissão,
banco com migration, auditoria, imagens e o pacote .nnp — e termina com o
módulo instalado e visível na Console.
Faça os passos na ordem. Cada passo tem o comando exato e a saída esperada; onde a saída depende do ambiente (endereços, tokens), o texto explica o que conferir em vez de inventar valores.
Antes de começar
- Git, JDK 26 e um runtime de contêiner na máquina. O Maven
Wrapper (
./mvnw) e a ferramenta (./nnconsolew) vêm com o projeto. - Para os passos de banco, um MySQL 8.0.35 ou 8.4 local.
- Para os passos 14 a 16 (upload e deploy), o endereço da Public API
(
NN_API_URL) e um token de plataforma (NN_TOKEN_CMDou o device flow) — é o operador quem faz esses passos.
Passo 1. Crie o módulo
Não há instalador do CLI: baixe o jar all publicado no repositório Maven da
empresa — br.com.nnumbers.console:console-sdk-cli (0.8.0-SNAPSHOT), no
grupo https://cicd.nnumbers.com.br/nexus/repository/maven-public/ (leitura
sem credencial; o grupo também espelha o Maven Central):
MAVEN=https://cicd.nnumbers.com.br/nexus/repository/maven-public
SDK=0.8.0-SNAPSHOT # a versão do SDK desta linha
value=$(curl -sS "$MAVEN/br/com/nnumbers/console/console-sdk-cli/$SDK/maven-metadata.xml" \
| awk '/<classifier>all<\/classifier>/{f=1} f && /<value>/{gsub(/.*<value>|<\/value>.*/,""); print; exit}')
curl -sS -o nnconsole-all.jar \
"$MAVEN/br/com/nnumbers/console/console-sdk-cli/$SDK/console-sdk-cli-$value-all.jar"
java -jar nnconsole-all.jar version
# sdk 0.8.0-SNAPSHOT (nnconsole/0.8.0-SNAPSHOT), contrato console.nnumbers.io/v1alpha3
(O caminho -SNAPSHOT do arquivo 404; por isso o value do <snapshotVersions>.)
Agora rode o gerador, apontando --dir para onde o módulo deve nascer — as
flags vêm ANTES do id posicional (a ordem contrária é recusada com
nnconsole: esperado exatamente um argumento: o id do módulo, exit 2):
java -jar nnconsole-all.jar module create --dir nnumbers-console-notes \
--package br.com.nnumbers.console.notes --name "Notes" notes
Dentro do módulo, o ./nnconsolew resolve o mesmo jar da versão do pom.xml
sozinho — o jar baixado só é necessário para este primeiro módulo.
O gerador escreve a árvore do módulo (57 arquivos) e não toca em mais nada. Você fica com um repositório pronto para commit:
nnumbers-console-notes/
├── pom.xml mvnw nnconsolew Makefile README.md .gitignore
├── .mvn/ configuração do Maven Wrapper
├── src/main/java/br/com/nnumbers/console/notes/
│ ├── Application.java o main (serve | migrate | version)
│ ├── module/Module.java o id e o marcador da release
│ ├── app/AppConfiguration.java a fiação (migrator, Actions, periódicas)
│ ├── app/Http.java as respostas JSON do serviço
│ ├── app/ItemsController.java as rotas de @NnsRoute
│ ├── app/ActionsController.java a rota de estado de Action
│ └── store/{Item,ItemStore,Migrations}.java o banco e as migrations
├── src/main/resources/migrations/0001_items.up.sql (+ MANIFEST)
├── src/test/java/…/ testes (identidade, rota, banco)
├── nnp/ release.yaml api.yaml iam.yaml audit.yaml placement.yaml (+ audit/)
├── nns/service.yaml nns/api/openapi.yaml nns/helm/chart/ nns/helm/testdata/
├── status/status.yaml embed/embed.yaml help/help.yaml
├── docs/docs.yaml docs/pt-BR/index.md docs/pt-BR/items.md
├── tests/conformance.yaml onboarding/targets-reference.yaml
A árvore gerada não inclui projeto de Status Probe nem surface (NNC): o
status/status.yaml descreve os componentes, e um módulo com UI ou com probe
próprio carrega o projeto no repositório, ao lado da árvore gerada (o Demo,
por exemplo, tem java/status-probe/ e nnc/).
Nada aqui é cópia: os descritores vivem um no lugar e o build os lê da raiz do repositório; o serviço os valida a cada partida, com os schemas do SDK.
Passo 2. Entenda a estrutura
Três camadas, nesta ordem:
- O serviço (
src/):Applicationsobe o runtime do SDK (serve), roda as migrations (migrate) ou imprime a release embutida (version).app/tem as rotas (@NnsRoute("items.list")) e o domínio;store/tem as instruções SQL, as migrations e os grants. - Os descritores (
nnp/,nns/service.yaml,status/,docs/,help/,embed/): o contrato da release.api.yamllista rotas,iam.yamlpermissões e papéis,audit.yamltrilhas e esquemas de evento,placement.yamlrecursos e réplicas,service.yamldependências de rede, slots de segredo, configuração e banco. - O empacotamento (
nns/helm/chart/,tests/): o chart que a plataforma renderiza e as expectativas da conformance.
Regra de ouro: se não está num descritor, não existe. O serviço não
declara caminho de rota, permissão nem limite de corpo no código — tudo isso
mora no api.yaml e é conferido na partida: rota do descritor sem método no
controlador impede o serviço de subir, com a mensagem de qual rota falta.
Passo 3. Rode os testes
./mvnw -q verify
O pom.xml gerado já declara o repositório Maven da empresa
(nnumbers-public, o grupo do Nexus que também espelha o Maven Central): com
um repositório local vazio e sem settings.xml, o ./mvnw resolve o
console-service-parent e o SDK de lá.
Com um MySQL 8.0 ou 8.4 disponível, aponte o DSN no formato
usuário:senha@tcp(host:porta)/ — a conta precisa de CREATE USER e de
ALL … WITH GRANT OPTION sobre `nnt\_%` (o teste cria e apaga um banco
nnt_notes_* próprio):
export NNC_TEST_MYSQL_DSN='nnstest@tcp(127.0.0.1:3306)/'
./mvnw verify
Saída esperada (trecho real):
[INFO] Running br.com.nnumbers.console.notes.store.ItemStoreMysqlTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.760 s -- in …ItemStoreMysqlTest
[INFO] Running br.com.nnumbers.console.notes.ApplicationTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.203 s -- in …ApplicationTest
[INFO] Running br.com.nnumbers.console.notes.app.ItemsControllerTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.077 s -- in …ItemsControllerTest
[INFO] Tests run: 15, Failures: 0, Errors: 0, Skipped: 0
[INFO] Running br.com.nnumbers.console.notes.ApplicationJarIT
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.751 s -- in …ApplicationJarIT
[INFO] BUILD SUCCESS
O que cada teste guarda: ApplicationTest valida os descritores embutidos e
as regras do contrato (toda permissão de rota existe no iam.yaml e é
coberta por um papel; a rota assíncrona tem rota de estado; as trilhas de
auditoria e a migration da release existem); ItemsControllerTest guarda a
autorização (com a permissão passa; sem ela, 403 permission_denied) e a
validação do corpo; ItemStoreMysqlTest roda o SQL em MySQL real (nome
repetido no tenant = 409, item de outro tenant = 404); ApplicationJarIT
prova o contrato de processo contra o JAR (version imprime a release e uso
inválido sai com código 2). Sem NNC_TEST_MYSQL_DSN, os testes de banco são
pulados com a mensagem; nunca fingidos.
Passo 4. Rode o serviço localmente
O runtime espera exatamente o que o Module Manager injeta em produção:
NN_* no ambiente, um arquivo de trust (quem pode chamar o serviço), um
Service Registry (endereços lógicos) e os slots de segredo em disco.
Para o desenvolvimento, você cria esses arquivos uma vez, fora do repositório
(.local/ é ignorado pelo git):
mkdir -p .local
# 1. a chave do gateway (o emissor das chamadas verificadas) e a identidade do serviço
./nnconsolew keys generate --issuer nnumbers-public-api --out .local/gateway.seed > .local/jwks.json
./nnconsolew keys generate --issuer nns:notes --out .local/notes.seed >/dev/null
# 2. o trust aponta para a chave pública que o comando acima imprimiu
cat > .local/trust.yaml <<'EOF'
apiVersion: console.nnumbers.io/v1alpha3
kind: TrustConfig
issuers:
- issuer: nnumbers-public-api
profiles: [ctx]
jwks: {file: /caminho/absoluto/para/.local/jwks.json}
EOF
# 3. o registry aponta para o seu MySQL local
cat > .local/registry.yaml <<'EOF'
apiVersion: console.nnumbers.io/v1alpha3
kind: ServiceRegistry
environment: development
services:
mysql.internal.nnumbers: {url: "mysql://127.0.0.1:3306"}
audit.internal.nnumbers: {url: "http://127.0.0.1:18999", audience: "internal:audit"}
EOF
# 4. os slots de segredo do banco (crie o banco e o usuário no seu MySQL primeiro,
# com GRANT OPTION para a conta de migração: ela concede a DML do runtime)
mkdir -p .local/secrets/db-runtime .local/secrets/db-migrator
printf '%s' notes_dev > .local/secrets/db-runtime/username
printf '%s' '<senha>' > .local/secrets/db-runtime/password
printf '%s' notes_dev > .local/secrets/db-runtime/database
cp .local/secrets/db-runtime/{username,password,database} .local/secrets/db-migrator/
printf '%s' 'notes_dev@127.0.0.1' > .local/secrets/db-migrator/runtime_account
chmod 600 .local/secrets/*/password
Com isso, rode as migrations (uma vez por banco) e suba o serviço:
export NN_ENV=development NN_MODULE_ID=notes NN_SERVICE_ID=notes NN_VERSION=0.1.0 NN_RELEASE_ID=notes@0.1.0
export NN_TRUST_FILE="$PWD/.local/trust.yaml" NN_SERVICE_REGISTRY_FILE="$PWD/.local/registry.yaml"
export NN_SECRETS_DIR="$PWD/.local/secrets" NN_IDENTITY_KEY_FILE="$PWD/.local/notes.seed"
java -jar target/nns-notes-0.1.0.jar migrate
java -jar target/nns-notes-0.1.0.jar serve
Saída esperada do migrate (mensagens reais): nnsmysql: migrations aplicadas, nnsmysql: grants do runtime conferidos, notes: migração concluída. Com o serviço no ar, em outro terminal:
curl -s http://127.0.0.1:8080/live
# {"status":"ok"}
curl -s http://127.0.0.1:8080/ready
# {"component":"notes","kind":"module","version":"0.1.0","release_id":"notes@0.1.0","role":"active",
# "status":"healthy","checks":[{"name":"db","status":"healthy"},{"name":"identity","status":"healthy"},
# {"name":"schema","status":"healthy"},{"name":"trust:nnumbers-public-api","status":"healthy"}],
# "pending_actions":0,"checked_at":"…"}
/live é o processo vivo; /ready é o serviço pronto (banco, schema,
identidade, trust e — quando o serviço tem rotas assíncronas — as Actions
pendentes). Em produção, são esses dois que o Kubernetes usa como sondas.
Para parar: Ctrl-C. O encerramento é gracioso (para de aceitar, drena e
sai com 0).
Passo 5. Acrescente uma rota
Uma rota nasce em dois lugares: o descritor e o controlador.
-
Declare no
nnp/api.yaml(id, método, caminho, permissão, idempotência, limite de corpo):- {id: items.get, method: GET, path: "/items/{item_id}", permission: notes.item.read} -
Escreva o método no controlador, sem caminho nem método HTTP — o
@NnsRoutecasa pelo id do descritor, e o runtime já conferiu token e permissão antes de chamar:@RestController@Profile("serve")class ItemsController {private final ItemsService service;/** {@code items.get}: o item do tenant; de outro tenant, 404. */@NnsRoute("items.get")ResponseEntity<byte[]> get(Ctx ctx, ServiceContext sc, Match m) {Item it = ItemStore.get(ctx, service.db, sc.requireTenantId(), m.params().get("item_id"));return Http.json(200, Map.of("item", ItemsService.view(it)));}} -
Espelhe a rota no
nns/api/openapi.yaml(o OpenAPI é conferido nos dois sentidos: rota sem operação e operação sem rota são erro):./nnconsolew openapi check --dir nnp # openapi: ok (5 rota(s)) -
Rode os testes. Rota do descritor sem método impede a partida, com a mensagem de qual rota ficou sem handler — o
./mvnw -q verifypega isso antes de qualquer deploy.
Dentro do método você recebe o que precisa do runtime: Ctx (correlação e
prazo da rota), ServiceContext (o contexto verificado: tenant, principal,
permissões), Match (parâmetros do caminho) e NnsBody (o corpo). Recusas
são sempre throw ApiError.of(...), que vira o erro do contrato com código,
reason e mensagem.
Passo 6. Acrescente uma Action
Operações demoradas não seguram a chamada: elas viram Actions, visíveis no Action Center, com estado consultável.
-
Declare a rota assíncrona no
nnp/api.yaml— sem parâmetro de caminho (o runner da conformance só chama rotas sem parâmetro; o item vai no corpo):- {id: items.refresh, method: POST, path: /items/refresh, permission: notes.item.refresh,idempotency: required, mode: async, max_body_bytes: 4096}A rota de estado já existe desde o passo 1 (
actions: {status_route: actions.get}). -
Na aceitação, crie a Action na mesma transação da resposta 202:
Action act = Actions.newAction(ctx, actionConfig, REFRESH_TYPE,itemId.isEmpty() ? Map.of() : Map.of("item_id", itemId));if (!itemId.isEmpty()) {act = act.withResource("item", itemId);}actions.create(ctx, tx, act);return Actions.acceptedResponse(act); // 202 + Location + {"action": …} -
Escreva o handler que o Runner executa, idempotente (repetido depois de crash, grava o mesmo resultado):
Map<String, Object> runRefresh(Ctx ctx, Action act) {RefreshInput in = StrictJson.unmarshal(act.input(), RefreshInput.class);// … recarrega o item (uma linha por item) …if (!ItemStore.recordRefresh(ctx, db, act.tenantId(), in.itemId(), now())) {throw ItemStore.notFound();}return Map.of("items", 1, "refreshed_at", …, "marker", Module.MARKER,"completed_by", config.releaseId());} -
Registre o handler no
ActionRunner(app/AppConfiguration.java):ActionRunner.builder(store, config.releaseId()).handler(ItemsService.REFRESH_TYPE, service::runRefresh).logger(logger.with("worker", "action-runner")).adopt(service::activeRole);
O Runner roda em toda réplica, faz poll, renova o lease, tenta de novo e
grava o evento terminal na trilha de auditoria (operations/item.refresh)
na transação da transição. Quando a release ativa troca, o Runner da nova
release adota as Actions que a antiga deixou em voo. No /ready, o campo
pending_actions diz quantas estão em aberto.
Passo 7. Proteja com uma permissão e um papel
-
Declare a permissão no
nnp/iam.yaml(id local, sem o produto na frente) e ligue-a a um papel:permissions:- {id: item.refresh, description: Iniciar a recarga assíncrona de um item}roles:operator: {description: "Leitura, criação e recarga",permissions: [item.read, item.create, item.refresh]} -
Use a permissão qualificada na rota (
notes.item.refresh) — é assim que o gateway a expande e o serviço a confere de novo. -
Escreva o teste de autorização (é o que o
ItemsControllerTestguarda):ServiceContext sc = context("items.refresh", "notes.item.refresh");assertThat(sc.can("notes.item.refresh")).isTrue();sc.require("notes.item.refresh"); // não lançaServiceContext denied = context("items.refresh" /* sem permissão */);assertThatThrownBy(() -> denied.require("notes.item.refresh")).isInstanceOfSatisfying(ApiError.class,e -> assertThat(e.code()).isEqualTo(Code.PERMISSION_DENIED));
Papéis tenant viram realm roles nnc:notes:<papel> nos realms de cliente
(o IAM garante as definições); papéis de plataforma existem só no realm
nnumbers-platform e são atribuídos pelo operador. Errar o scope na primeira
release é a decisão mais cara de mudar depois: permissão que já foi
platform nunca volta a tenant.
Passo 8. Acrescente uma tabela com uma migration
-
Escreva o arquivo em
src/main/resources/migrations/, um por versão, com uma instrução e o nomeNNNN_<nome>.up.sql:CREATE TABLE notes_items (id CHAR(36) NOT NULL,tenant_id VARCHAR(64) NOT NULL,name VARCHAR(80) NOT NULL,refreshed_at DATETIME(3) NULL,created_at DATETIME(3) NOT NULL,PRIMARY KEY (id),UNIQUE KEY notes_items_name (tenant_id, name),KEY notes_items_list (tenant_id, created_at, id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci -
Liste o arquivo no
MANIFESTda pasta (o conjunto é fixado por lista — o serviço não varre diretório em tempo de execução). -
Declare os grants da DML do runtime em
store/Migrations.java:public static final List<TableGrant> GRANTS =List.of(TableGrant.of("notes_items", "SELECT", "INSERT", "UPDATE", "DELETE"));
Migrations são expand-only: uma aplicada nunca muda (o checksum fica no
ledger nn_schema_migrations e é conferido a cada partida); release nova só
acrescenta arquivo. É isso que permite upgrade e rollback sem perder
dados. No migrate, a conta de migração aplica o DDL e concede a DML do
runtime; o serviço nunca tem DDL. O teste ItemStoreMysqlTest cobre isso em
banco real, e ele só passa se a migration, o MANIFEST e os grants estiverem
certos.
Passo 9. Emita um evento de auditoria
-
Declare a trilha e o evento no
nnp/audit.yaml, com o schema do payload:trails:items:audience: tenantevents:item.create: {schema_version: 1, description: Item criado,payload_schema: audit/items/item.create.v1.schema.json} -
No schema (
nnp/audit/items/item.create.v1.schema.json), só os campos que o evento leva. -
Emita dentro da transação da mudança de domínio:
AuditEvent ev = AuditEvent.newEvent(ctx, Origin.publicRoute(sc), auditConfig,"items", "item.create", 1).withResource("item", it.id()).withDetail("name", it.name());emitter.emit(ctx, tx, ev);
O evento vai para o outbox na mesma transação do INSERT: ou os dois
existem, ou nenhum. O SDK entrega o outbox com a identidade nns:notes.
Leituras não auditam; as operações assíncronas ganham o evento terminal no
fim (passo 6). O contrato é append-only: uma trilha nova pode ser acrescentada,
mas o schema de um evento já registrado nunca muda — crie a versão seguinte
(item.create v2) se o payload precisar crescer.
Passo 10. Cuide da saúde
O serviço já responde /live e /ready desde o passo 1 (são rotas
embutidas, sempre liberadas; não passam pelo gateway nem por permissão).
Além delas, o módulo declara o Status Probe: um componente por pergunta
"como sei se o produto funciona?".
# status/status.yaml
components:
- {id: api, display_name: "API", audience: public, impact: major,
description: "Notes: API de itens e recargas"}
O probe é uma imagem própria (java/status-probe/), sem framework, que olha
o /ready do NNS da própria release:
public static void main(String[] args) {
StatusProbe.main(new Probe.Options(), (ctx, p) -> p.check("api", Checks.ready(p.self())));
}
O componente audience: public aparece na página pública de status; os demais
ficam na Platform Console. Configure o intervalo e os limiares no
status/status.yaml (o avaliador usa os mesmos valores para decidir que uma
observação está velha).
Passo 11. Construa a imagem do serviço
./mvnw spring-boot:build-image
A imagem é construída pelo Paketo (non-root, JVM), sem Dockerfile. Duas
coisas do ambiente: um runtime de contêiner na máquina, e — para publicar
— a credencial de registry (fornecida pela equipe da plataforma num DOCKER_CONFIG;
nunca docker login -p) e o PUSH=1:
PUSH=1 make image # publica e mostra o digest
Guarde a referência publicada, por digest, em nns/build/image.ref; o
pacote lê a imagem daí (a tag é só conveniência; o que vale é o digest). O
Status Probe tem imagem própria (make status-image e
status/build/image.ref) — mas o projeto do probe é seu: a árvore gerada
não o traz, e o alvo explica isso enquanto ele não existir (veja §Status
Probe do README do módulo). O Makefile recusa construir com a árvore suja:
o digest tem de corresponder ao commit empacotado.
Passo 12. Empacote o NNP
make package
O alvo roda o nnp build com os descritores, o chart, as docs, o help e o
SBOM, e depois o nnp verify:
./nnconsolew nnp build --root . --out dist \
--java-image "$(cat nns/build/image.ref)" \
--probe-java-image "$(cat status/build/image.ref)"
./nnconsolew nnp verify --signature-policy off dist/notes-0.1.0.nnp
O resultado é dist/notes-0.1.0.nnp: o pacote único da release, com a árvore
de descritores, o chart, as imagens (por digest), as páginas de docs, os
tópicos de help, o SBOM e o release.json que amarra tudo ao commit. O mesmo
commit produz o mesmo pacote.
O make package lê as imagens publicadas por digest
(nns/build/image.ref e status/build/image.ref): sem um runtime de
contêiner na máquina não há como construí-las, sem a credencial de
registry — que a equipe da plataforma fornece num DOCKER_CONFIG — não há como
publicá-las, e a assinatura (nnp sign, passo 13) é do operador, com a
chave dele. O seu caminho local termina em dist/notes-0.1.0.nnp.
Passo 13. Valide o pacote
./nnconsolew nnp verify --signature-policy off --json dist/notes-0.1.0.nnp \
| jq -r '.report.checks[] | "\(.id) \(.status)"'
O verify confere o que o Module Manager vai conferir: limites e nomes das
entradas, duplicatas, allowlist de conteúdo, canonicidade do pacote, checksums,
metadados da release, descritores, integridade das docs e do chart,
compatibilidade de SDK, SBOM e o scan de segredos. Para uma release de
produção, o pacote é assinado pelo operador e a validação usa a política
required:
./nnconsolew nnp sign --key-file <seed do operador> --out dist/notes-0.1.0.signed.nnp dist/notes-0.1.0.nnp
./nnconsolew nnp verify --signature-policy required --trusted-keys <jwks> dist/notes-0.1.0.signed.nnp
O nnp verify local é a sua rede de segurança; o Report do Module Manager
(passo 15) é o veredito.
Passo 14. Faça o upload
Esta parte é do operador (a identidade é de plataforma):
export NN_API_URL=https://api-nn.nnumbers.com.br
export NN_TOKEN_CMD='<comando que imprime o token no stdout>' # ou o device flow
./nnconsolew modules upload dist/notes-0.1.0.signed.nnp --api "$NN_API_URL"
O comando acompanha a Action de validação até o fim e imprime o estado da
release. Repetir com a mesma Idempotency-Key é seguro; o padrão é gerar
uma nova. O upload valida o pacote e guarda a release — ele não provisiona
nada.
Passo 15. Instale o módulo
./nnconsolew modules plan notes notes@0.1.0 # READY ou os bloqueios, com dono e remediação
./nnconsolew modules deploy notes notes@0.1.0 --api "$NN_API_URL"
O primeiro deploy cria o bootstrap (namespace, banco, principais, segredos,
grants) e faz o stage e a ativação da release: NNS, Status Probe, rotas no
gateway, permissões, auditoria, docs e help. Tudo numa Action só, com fases
(PREFLIGHT → PROVISIONING → MIGRATING → DEPLOYING → REGISTERING → ACTIVATING → ACTIVE). Falha com checkpoint é repetível: deploy de novo retoma de onde
parou, sem criar nada duas vezes.
Passo 16. Veja o módulo na Console e acesse
Depois da ativação:
-
Em
./nnconsolew modules show notes notes@0.1.0, a release estáACTIVEcom os checks de rede, status, docs e helppassed. -
A Platform Console lista o módulo em PLATAFORMA → Módulos, com a release ativa; as páginas de docs da release aparecem no portal e os tópicos de help aparecem no Help Panel de quem tem a permissão.
-
As rotas respondem pelo gateway, com o token de quem chama — é o gateway que autentica e o serviço que confere a permissão outra vez:
curl -s "$NN_API_URL/v1/notes/items" -H "Authorization: Bearer $TOKEN"# {"items": []} # logo depois do deploycurl -s -X POST "$NN_API_URL/v1/notes/items" \-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \-H "Idempotency-Key: 0d1f4a2c-8f0b-4b6e-9d5a-1c2b3a4d5e6f" \-d '{"name": "Primeiro item"}'# {"item": {"id": "…", "name": "Primeiro item", "refreshed_at": null, "created_at": "…"}}
O notes nasce sem interface própria (nenhuma surface declarada): ele aparece
como serviço de plataforma e pela API. Um módulo com UI declara a surface no
release.yaml e o front é carregado na Console — o ciclo de vida (upload,
deploy, rollback) é o mesmo.
Passo 17. Publique uma nova versão
Suba a versão nos lugares que a carregam juntos — nnp/release.yaml,
nns/api/openapi.yaml, nns/helm/chart/Chart.yaml,
nns/helm/testdata/injected-values.yaml e o marcador em Module.java — e
aponte a N-1 nas expectativas da conformance:
# tests/conformance.yaml
release: {previous: notes@0.1.0}
Depois, o mesmo caminho do primeiro deploy:
./mvnw -q verify && make package
./nnconsolew modules upload dist/notes-0.1.1.signed.nnp --api "$NN_API_URL"
./nnconsolew modules deploy notes notes@0.1.1 --api "$NN_API_URL"
O upgrade só reaplica o que a release muda: o bootstrap é reconferido
(exists_ok), não recriado. As migrations continuam expand-only: a N-1 e a N
enxergam o mesmo banco.
Passo 18. Volte para a versão anterior
./nnconsolew modules rollback notes --to notes@0.1.0 --api "$NN_API_URL"
O rollback é acoplado: volta a release inteira (NNS, probe, rotas,
permissões, docs, help e rede da release) para a N-1, de uma vez. Ele reverte
só recursos de release: namespace, banco, principais e segredos são do
módulo e continuam (o plano seguinte os mostra exists_ok). Por isso as
migrations são expand-only e nenhum item muda de audience entre releases
consecutivas: crie item novo em vez de mudar o que já existe.
Na primeira release da linha não há N-1 para onde voltar: ela declara
release: {previous: none, previous_reason: no_same_line_previous} e os itens
de rollback saem not_applicable com o motivo; a saída, se precisar, é
disable ou uma versão nova.
Próximos passos
Você tem um módulo no ar. Para o ciclo completo — repositório e aprovações, domínio, descritores, provas locais, publicação, provas ao vivo, upgrade, rollback e o relatório final — siga o ciclo de vida de um módulo.
Esta página ajudou?
Reportar um problema nesta páginaNão envie senhas, chaves, tokens ou dados de clientes.