Integração com outro serviço
Quando usar: quando o módulo precisa avisar, consultar ou comandar outro serviço. O código conhece nomes lógicos; o endereço, o TLS e a audiência vêm da configuração do ambiente.
1. Declare a dependência
nns/service.yaml:
dependencies:
- {name: mysql.internal.nnumbers}
- {name: notifier.internal.nnumbers}
Sem a declaração, não existe rede de saída para esse destino: o egress do pod é só o declarado
(a política é gerada pelo Module Manager a partir deste arquivo, e o destino precisa estar no catálogo
do deployment target). Declarar no service.yaml e não ter o destino no ambiente é o erro mais comum
de primeira integração.
2. Tire o endereço do Registry
java/src/main/java/.../outbound/NotifierClient.java:
/** O nome lógico do destino, como está em `nns/service.yaml` e no Service Registry. */
public static final String REGISTRY_NAME = "notifier.internal.nnumbers";
/** O escopo que o destino exige e que o teto do emissor precisa cobrir. */
static final String SCOPE = "sample:notify";
static final String PATH = "/v1/announcements";
static final int MAX_BODY = 1 << 20;
private final RegistryHttp http;
private final Signer signer;
private final NnsConfig config;
private NotifierClient(RegistryHttp http, Signer signer, NnsConfig config) {
this.http = http;
this.signer = signer;
this.config = config;
}
/**
* O cliente do ambiente: resolve o destino no Registry e passa a usar a identidade da release.
*
* @throws br.com.nnumbers.console.sdk.registry.RegistryException o destino não está no Registry
* deste ambiente
*/
public static NotifierClient of(Registry registry, Signer signer, NnsConfig config) {
return new NotifierClient(
RegistryHttp.of(registry, REGISTRY_NAME, Duration.ofSeconds(15)), signer, config);
}
O Registry e o Signer são beans do SDK: o primeiro é a config de nomes lógicos do ambiente; o
segundo é a identidade desta release (nns:<id>), carregada do arquivo de seed.
3. Assine a chamada
@Override
public void announce(Ctx ctx, String event, Map<String, Object> body) {
byte[] payload;
try {
payload = Json.mapper().writeValueAsBytes(body);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("sample: aviso não serializável", e);
}
Headers h = new Headers();
h.set("Content-Type", "application/json");
h.set("X-Sample-Event", event);
h.set("X-Sample-Signature", "v1=" + sign(payload));
h.set("X-Correlation-Id", correlation(ctx));
h.set("Authorization", "Bearer " + token(correlation(ctx)));
RegistryHttp.Reply reply = http.send(ctx, "POST", http.url(PATH), h, payload, MAX_BODY);
if (reply.status() < 200 || reply.status() > 299) {
throw new IllegalStateException("notifier: status " + reply.status());
}
}
/**
* O token de identidade da chamada: perfil `svc`, `aud` do Registry, escopo exigido pelo destino e
* vida CURTA (o padrão do perfil é 120 s; o teto é 300 s).
*/
String token(String corr) {
return signer.signSvc(http.endpoint().audience(), SCOPE, Map.of("corr", corr), Duration.ofSeconds(120));
}
Cinco decisões que valem a pena notar:
- Um token NOVO por chamada, com vida curta. Não existe token de serviço de longa duração.
- A audiência vem do Registry (
http.endpoint().audience()), não de uma string no código: é o que permite o mesmo código falar com o destino de desenvolvimento e o de produção. - O escopo é o que o destino exige (
sample:notify) — pedir escopo a mais só aumenta o poder. - O
X-Correlation-Idpropaga ocorrda requisição original: sem ele, um erro no destino não se liga ao pedido do cliente. - O corpo vai assinado com a chave do slot (veja Segredo): identidade responde "quem está chamando"; assinatura responde "o corpo é esse mesmo".
O timeout do cliente (15 s acima) e o prazo do Ctx valem juntos: o menor manda.
4. Use com bom senso
O aviso é melhor-esforço: o destino fora do ar não pode desfazer uma varredura já gravada.
private void announce(Map<String, Object> result, int score) {
try {
notifier.announce(
Ctx.background().withTimeout(Duration.ofSeconds(5)),
"item.scan.finished",
Map.of("item_id", result.get("item_id"), "score", score, "engine", ScanEngine.ENGINE));
} catch (RuntimeException e) {
logger.atWarn()
.addKeyValue("item_id", result.get("item_id"))
.addKeyValue("error_class", e.getClass().getSimpleName())
.log("sample: aviso ao notificador não saiu");
}
}
O cliente é alcançado por uma interface (Notifier), e é ela que o domínio usa: isso é o que
permite testar o handler sem rede e mudar a política de falha num lugar só.
Como testar
O teste sobe um destino de verdade na JVM e confere o que sai — o token é verificado com o verificador do próprio SDK:
@Test
void theCallCarriesAServiceIdentityTokenThisDestinationAccepts() throws Exception {
client().announce(Ctx.background().withTimeout(Duration.ofSeconds(5)), "item.scan.finished",
Map.of("item_id", "0be1d3c6-6f3e-4a6b-a4f5-2a2f2a9f0d10", "score", 75));
String token = seenAuth.get().substring("Bearer ".length());
// O token é verificado com o MESMO verificador do SDK, contra o trust do destino: assinatura,
// perfil, `aud` e `scope` — não basta "tem um Bearer".
Path trust = TestKeys.writeTrust(dir.resolve("trust"),
List.of(new TestKeys.Issuer(ISSUER, "svc", TestKeys.key("nns:sample"), List.of(NotifierClient.SCOPE))));
Verifier verifier = Verifier.of(Trust.load(trust, null, "development", EmbeddedSchemaCheck.INSTANCE));
Token t = verifier.verify(Ctx.background(), token,
Want.of(Profile.SVC, "internal:notifier").withScopes(NotifierClient.SCOPE));
assertThat(t.issuer()).isEqualTo("nns:sample");
assertThat(t.hasScope(NotifierClient.SCOPE)).isTrue();
}
O destino é um HttpServer local, registrado num registry.yaml de teste que o cliente lê como
qualquer outro ambiente:
services:
notifier.internal.nnumbers:
url: "http://127.0.0.1:8080"
audience: "internal:notifier"
E o teste de recusa — destino que responde erro falha a chamada (quem chama decide o que fazer):
@Test
void aDestinationThatRefusesFailsTheCall() {
answer = 503;
assertThatThrownBy(() -> client().announce(ctx, "item.scan.finished", Map.of("item_id", "x", "score", 0)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("503");
}
Erros comuns
- Dependência não declarada — a chamada não sai (política de rede) e o erro aparece como
dependency_unavailable. Declare emnns/service.yamle confirme o destino no catálogo do ambiente. - Host, IP ou
namespace.svcno código — quebra a portabilidade e a revisão de rede: o código conhece o nome lógico, só. - Token de vida longa ou reaproveitado entre chamadas — um token por chamada, curto. O teto do
perfil
svcé 300 s. - Escopo largo ("
*" ou o escopo do destino inteiro) — peça o escopo que a operação exige. - Chamar outro serviço dentro da transação do banco — a transação segura linha e conexão enquanto a rede demora; chame fora dela.
- Repetir a chamada sem idempotência do outro lado — retry é esperado; o destino tem de suportar a repetição (idempotência ou operação naturalmente idempotente).
- Deixar a exceção subir sem classificar — o cliente do seu módulo recebe 500 sem texto; decida se
é
dependency_unavailableretryable ou uma falha de negócio. - Assinar o corpo depois de alterá-lo — assine os bytes que você envia, e não serialização diferente da que foi assinada.
- Achar que a identidade de serviço dá tenant — ela não tem tenant: quem chama é o módulo.
Quando a operação precisa do usuário, ela é do serviço chamador via
obo, e quem decide é o destino.
Esta página ajudou?
Reportar um problema nesta páginaNão envie senhas, chaves, tokens ou dados de clientes.