Contribuir
Acrescentar um Kind
Antes de leres: Convenções de código, Arquitectura e Os crates — esta página assume que sabes o que o delonix-stack possui e porque é que o planeamento é puro.
Um Kind é um recurso declarativo que este motor conhece — Container, Volume, Service, e por
aí fora (o delonix api-resources lista-os todos). Acrescentar um toca em mais do que um simples
braço de match: a tabela que descreve o que o Kind É, o código que o aplica, e a fiação do
reconciliador que deixa o stack plan/apply tratá-lo como qualquer outro Kind. Esta página
percorre isso pela ordem, com um Kind real — Service (ADR-0032) — como exemplo trabalhado do
princípio ao fim. Não é o Kind mais recente (o IPPool, o NetworkGateway, o NetworkZone e o
RuntimePolicy vieram depois), mas é o que exercita todos os caminhos de uma vez: primário,
convergente, removível, com namespace e com registo próprio. Todo o ficheiro citado abaixo é lido
da árvore, não da memória de uma disposição antiga.
A única tabela a que um Kind tem de responder#
crates/contexts/delonix-stack/src/kinds.rs é a fonte única do que um Kind é. O próprio
comentário do módulo explica porque existe: antes desta tabela, os mesmos factos sobre um Kind
viviam em seis listas separadas (KINDS, CONVERGING_KINDS, TEARDOWN_KINDS,
kind_honors_namespace, os DECLARATIVOS do teste do wait, e os braços do presence()), e
divergiam — o Vm, o FirewallPolicy e o ShareVolume ganharam uma vez um adaptador de
reconciliador e ficaram DE FORA de CONVERGING_KINDS, por isso o passo de convergência
saltava-os em silêncio enquanto o próprio apply deles mantinha o recurso a parecer correcto,
pelo caminho errado.
Acrescentar um Kind começa por uma linha KindFacts:
pub const SERVICE: &str = "Service";
KindFacts {
kind: SERVICE,
plural: "services",
short: &["svc"],
api_version: "networking.delonix.io/v1alpha1",
domain: Domain::NetConnectivity,
form: Form::Primary,
in_stack: true,
stack_group: "services",
converges: true,
teardown: true,
namespaced: Namespaced::Always,
presence: Presence::Registry,
},
Cada campo é uma decisão, não uma formalidade:
| Campo | Pergunta a que responde | Errá-lo |
|---|---|---|
kind |
O nome, como const — nunca um literal de string à solta. Uma renomeação a tocar em todos os sítios de chamada foi medida em 106 ocorrências em dez ficheiros antes de esta tabela existir; um const transforma um typo num erro de compilação em vez de um braço de match silenciosamente inalcançável (um padrão &'static str mal escrito degrada para uma binding fica-com-tudo, o que o próprio comentário do SECRET/NETWORK/etc. na tabela nomeia explicitamente). |
Um segundo Kind a responder a ninguém, ou uma renomeação que falha um sítio sem erro de build nenhum. |
plural |
A palavra que delonix get <plural> aceita. Campo próprio, não kind + "s" — Dependency→dependencies e Ingress→ingresses não seguem essa regra. |
delonix get services (correcto) contra um delonix get servicess adivinhado. |
short |
Abreviaturas aceites. Escasso de propósito — um teste impõe unicidade global, por isso uma abreviatura que colida com outro Kind falha o build em vez de sombrear em silêncio. | Dois Kinds a responder às mesmas três letras. |
api_version |
O apiVersion que um manifesto escreve, dividido por domínio (compute.delonix.io/…, networking.delonix.io/…, …) desde a ADR-0020. Uma coluna, não uma constante partilhada, precisamente para os Kinds poderem migrar para o esquema dividido um de cada vez. |
Um Kind preso à string legada delonix.io/v1, ou no grupo errado. |
domain |
A área de actuação, mostrada na coluna DOMAIN do stack ls/plan --fields. As três de rede estão separadas de propósito: NetConnectivity responde «existe um caminho», NetPolicy responde «o tráfego nele é permitido» — fundi-las esconderia que o NetworkRoute abre um caminho enquanto o FirewallPolicy decide se deixa o tráfego atravessá-lo. |
Um domínio que responde a uma pergunta diferente da que o Kind de facto actua sobre. |
form |
O que um documento deste Kind se torna: Primary (o seu próprio apply, sobrevive ao load), Sugar(alvo) (reescrito para outro Kind no momento do load, desaparece), Aggregate (expande-se nos documentos que contém, como o Stack), Compat(alvo) (um schema estrangeiro — o Ingress é networking.k8s.io/v1 — compilado sobre o mecanismo de outro Kind, e ao contrário do Sugar sobrevive ao load), ou Sunset(alvo) (ainda primário, sobrevive ao load, mas um sucessor é anunciado; usado quando reescrever mudaria em silêncio o que o motor faz — o Container não pode baixar para um Pod de um membro só porque um Pod constrói sempre uma netns partilhada, o que é uma forma de execução diferente). |
Escolher Sugar para algo que tem de manter o seu próprio apply, ou vice-versa. |
in_stack |
Se o stack apply sequer trata dele. As linhas com in_stack: true têm de ficar um prefixo contíguo da tabela — o destroy deriva a sua ordem de teardown invertendo a ordem do stack, por isso uma linha colocada depois de um Kind fora do stack muda a ordem de apply sem ninguém editar uma «ordem» em lado nenhum. Um teste (os_kinds_do_stack_sao_um_prefixo_contiguo) impõe isto. |
Um Kind aplicado fora de ordem de dependência, ou um Kind de procedimento remoto como o KubernetesCluster (SSH contra hosts que já existem, não um recurso local) fiado por engano no ciclo. |
stack_group |
A chave do spec de um kind: Stack que guarda documentos deste Kind (services:), ou "" quando não pode ser agrupado. Esta coluna governa a expansão: o expand_stack, o schema, o aviso de campo desconhecido e a documentação gerada lêem-na todos, e não há uma segunda lista de grupos (vê O grupo do Stack abaixo). Não é a mesma pergunta que in_stack — o Workload e o Dependency baixam no load, por isso não são aplicados como eles próprios, mas os dois são coisas que uma pessoa escreve dentro de um Stack. |
Um Kind que o stack apply trata e que não pode ser posto dentro de um Stack — foi o que aconteceu ao NetworkRoute, ao NetworkAccessRule, ao Service e ao App enquanto a lista de grupos era escrita à mão. |
converges |
Se um campo mudado é de facto aplicado, contra só «garante presente». false é legítimo — o estado do Secret são valores cifrados que um plano não vai decifrar para comparar — mas precisa de uma razão (vê o not_converged_reason abaixo); uma desculpa genérica falha um teste. |
Um Kind que reporta ! em todos os planos com uma razão que se lê como «ninguém chegou lá» quando a verdade é uma propriedade do recurso. |
teardown |
Se o destroy_one o consegue remover, para o --prune e o destroy o poderem prometer. Um teste (so_um_kind_convergente_tem_teardown) recusa um Kind com teardown: true e converges: false — prometer podar algo que o plano nem consegue representar como mudado. |
O --prune a prometer remoção e o destroy_one a recusar a meio, depois de Kinds anteriores na ordem de teardown já terem desaparecido. |
namespaced |
Never, Always, ou PerDocument. Não é um bool — o Volume tem genuinamente três respostas: nenhuma para um volume simples, real para um com um bloco share:; modelá-lo como true avisaria «namespace sem efeito» em todo volume normal, e como false avisaria o mesmo, erradamente, numa share cujo namespace decide em que directório os seus dados vivem. |
Um aviso de namespace que contradiz o que o próprio apply do Kind faz com o campo. |
presence |
Como stack ls/wait sabem se o recurso existe: Registry (um store responde sim/não), Derived (calculado a partir de outra coisa — um Pod são os seus membros com label), Declarative (nada para reler; o recurso é uma directiva aplicada a um alvo, e presence() responde -, que não é «ausente»), ou NotObservable (nunca chega ao presence() — não sobrevive ao load, ou não é sequer um recurso local). |
O NetworkRoute já não teve braço nenhum no presence() e caía em _ => ("?", "unsupported kind") — impresso por ls/describe, e lido pelo wait como pendente para sempre. |
A linha do Service lê-se, numa frase: aplicado pelo stack, logo a seguir aos Kinds de compute
que selecciona; agrupado sob services: num Stack; tem um caminho real (NetConnectivity);
primário; converge sem recriar; pode ser desfeito; sempre namespaced; e um registo real está por
trás.
O grupo do Stack#
Um Kind com in_stack: true tem de ter um stack_group, e quatro testes em kinds.rs seguram a
coluna no sítio (ADR-0045):
every_kind_applied_by_the_stack_has_a_group— nada do que o stack aplica pode faltar emkind: Stack.a_kind_without_a_group_says_why— uma linha comstack_group: ""precisa de uma entrada emstack_group_absent_reason(o próprioStack, oKubernetesCluster), e uma linha com grupo não pode ter uma.a_group_key_is_unique_and_no_alias_shadows_it— dois Kinds sob uma chave fundiriam os seus filhos e entregariam a um deles a spec errada.a_group_is_the_plural_of_its_kind— a chave é o plural em lowerCamelCase (networkRoutesparaNetworkRoute), por isso ninguém precisa de a ir procurar. Só três chaves mais antigas ficam isentas (ingress,vms,firewallPolicies), porque renomear um grupo parte todo Stack publicado.
O grupo tem depois de ser provado de ponta a ponta, em bins/delonix-runtime-bin/src/cmd/:
manifest.rs,stack_group_sample— aspecmínima de um filho do grupo e o Kind em que aterra; oevery_stack_group_loadspercorre todo grupo e falha num sem amostra.examples/stack.yaml— tem de mencionar o grupo; othe_stack_example_names_every_groupfalha senão. Este ficheiro é também o que a página de Kinds do site do utilizador mostra.schema.rs,every_stack_group_is_typed_against_its_kinds_own_spec— os itens do grupo são tipados contra a própria spec do Kind, por isso o Kind precisa do seu braço emTYPED_KINDS(secção seguinte) antes de o seu grupo poder validar.
O tipo de spec e o schema#
Um Kind com spec de manifesto tipada (a maioria) precisa de uma struct
#[derive(Deserialize, Serialize, JsonSchema)] — ServiceSpec para este exemplo, em
bins/delonix-runtime-bin/src/cmd/service.rs — e um braço em TYPED_KINDS, em
bins/delonix-runtime-bin/src/cmd/schema.rs, mais o braço correspondente que nomeia a struct para o
manifest_schema. Essa constante alimenta o delonix manifest schema e o
delonix explain <Kind>.<campo>, os dois gerados da mesma struct (ADR-0007), para o schema
publicado nunca poder divergir do que o código de facto aceita. Deixar um Kind de fora é um
estado real e permitido — o Storage e o ShareVolume não têm schema de propósito, porque são
reescritos para Volume no load e uma segunda struct seria só uma cópia mantida à mão dos campos
que o próprio Volume já tem — mas tem de ser dito: untyped_hint(kind) dá o redireccionamento
específico, e o todo_kind_conhecido_tem_schema_ou_dica (em schema.rs) falha o build se um Kind
que a tabela conhece não estiver nem em TYPED_KINDS nem tiver uma dica. A mensagem genérica
("no typed schema for X") lê-se como um bug do manifesto; a dica diz que é uma propriedade do
Kind.
Há mais dois sítios que lêem a spec, ambos em bins/delonix-runtime-bin/src/cmd/manifest.rs:
filled_spec— um braço que chama ospec_with_defaults(doc)do Kind, o round trip pela struct tipada que ostack apply --dry-rune omanifest renderimprimem com todo default preenchido.spec_fields_for— um braço que devolve a lista*_SPEC_FIELDSdo Kind, que é contra o que owarn_unknown_fieldsverifica um documento. OadditionalProperties: falsedo schema tira as suas chaves aceites da mesma lista, por isso um typo num nome de campo é apanhado nos dois sítios.
Depois regenera o schema publicado, porque é um ficheiro que um editor vai buscar, não uma cópia que alguém mantém à mão:
delonix manifest schema > docs/schema/v1/delonix.json
O o_schema_publicado_esta_em_dia_com_o_codigo (em schema.rs) falha até o ficheiro ser
exactamente o que o binário gera. Usa o binário construído da tua árvore
(target/release/delonix ou cargo run -p delonix-runtime-bin --), não o que está no teu PATH.
Fiar o Kind no reconciliador#
O plano do reconciliador (crates/contexts/delonix-stack/src/reconcile.rs::plan) é puro — nunca
abre um store nem corre um comando. Tudo o que toca na máquina vive em
bins/delonix-runtime-bin/src/cmd/stack.rs, re-exportado como cmd::kinds/cmd::reconcile a
partir do delonix-stack (pub use delonix_stack::kinds; em cmd/mod.rs, por isso nada que já
chamasse cmd::kinds::… teve de mudar quando a tabela passou para o seu próprio crate). Quatro
funções em stack.rs precisam de um braço para um Kind novo que converge:
desired_of— umreconcile::Desiredpor documento, comfieldschaveados pelo nome do campo do manifesto (matchLabels,port), nunca pelo nome do registo interno, porque o diff é lido por quem escreveu o YAML.Service::desiredconstrói isto a partir deServiceSpec.actual_of— todas as instâncias do Kind que existem na máquina, para o--pruneter com que comparar.Service::actuallêdelonix_sdn::infra::service_list()e preenche os mesmos nomes de campo que odesiredusou, senão o diff compara alhos com bugalhos.converge_and_stamp— aplica umAction::Updatea quente. OServicereaproveita aqui o seu próprioapply_one(converge_doc), porque essa função já sobrescreve por inteiro a entrada do registo — a mesma forma queFirewallPolicyeNetworkAccessRuleusam, e a mesma razão: um caminho por-campo separado seria uma segunda maneira de escrever o mesmo registo, e duas maneiras começam a discordar. Um Kind sem caminho de actualização ao vivo nenhum (oPodnão tem campo quente nenhum) devolve o erro explícito "no live update path" em vez de cair em silêncio.stamp_all— regista a posse (labeldelonix.io/stack) e o mapa de campos aplicado (anotaçãodelonix.io/last-applied) depois de um apply com sucesso, o que é o que transforma o plano seguinte num diff de três vias em vez de duas.Service::stampescreve-os directamente na própria entrada de registo do serviço — ao contrário doNetworkAccessRule, cuja regra vive num container que não é dele, umServicetem um registo inteiramente seu.
E o destroy_one precisa de um braço a chamar remove_for_replace quando teardown: true — o
Service::remove_for_replace só chama delonix_sdn::infra::service_remove.
Nada disto é inventado por Kind. O run_layers (também em stack.rs) é onde um documento é
de facto criado pela primeira vez, uma camada por Kind in_stack, pela ordem da tabela
(layers.run(k::SERVICE, "🧭", || super::service::apply(docs))?) — essa função apply(docs) é a
mesma que o grupo imperativo da CLI já tem, reaproveitada em vez de duplicada.
Campos que convergem sem recriar#
Se converges: true, acrescenta uma entrada a hot_fields em reconcile.rs a nomear
exactamente que campos do manifesto podem mudar ao vivo. Esta tabela é uma promessa que o
executor tem de cumprir — listar um campo que o passo de convergência não consegue de facto
aplicar transforma um Replace limpo num Update que falha a meio, o que o comentário do módulo
chama "estritamente pior do que declarar o replace à cabeça". O Service lista
["matchLabels", "port"], porque service::apply_one já sobrescreve por inteiro o registo sem
precisar de reiniciar nada — nada num Service é frio. Um campo deixado fora de hot_fields
continua a aparecer no plano; só força Action::Replace (recusado sem --replace <Kind>/<nome>)
em vez de Action::Update.
Três testes em cmd/stack.rs mantêm esta tabela honesta contra a tabela de kinds.rs e umas
contra as outras:
as_tres_listas_de_kinds_convergentes_concordam— todo Kind marcadoconverges: truetem de aparecer na saída de--fields(compared_fields_table), e vice-versa; e um Kind convergente nunca pode carregar a desculpa genérica denot_converged_reason.todo_kind_nao_convergente_tem_razao_especifica— o inverso: todo Kind que NÃO converge precisa da sua própria frase concreta emnot_converged_reason, nunca oNOT_CONVERGED_GENERICpartilhado.todo_kind_convergente_tem_teardown_ou_razao— um Kind convergente ou é removível (kinds::has_teardown) ou tem uma entrada emno_teardown_reason; nunca os dois, nunca nenhum.
Presença, para ls/describe/wait#
Acrescenta um braço a presence(kind, doc, containers) em stack.rs que diga se uma instância
existe e, se sim, a sua string de estado. O braço do Service verifica
delonix_sdn::infra::service_get(namespace, nome) e, se encontrado, conta quantos containers
vivos o seu selector alcança neste momento (lendo o mesmo índice que o próprio resolvedor de
DNS lê, para o plano nunca poder discordar do que um cliente a pedir o nome de facto recebe).
Dois testes guardam esta coluna especificamente:
todo_o_kind_declarativo_de_kinds_tem_braco_no_presence— todo Kind cujopresencesejaDeclarativeresponde de facto-a partir depresence()(não o"?"/"unsupported kind" de recurso, em que oNetworkRoutecaía antes de ganhar um registo próprio).um_kind_declarativo_nao_fica_pendente_para_sempre—is_pending("-", kind, estado)éfalsepara todo Kind declarativo. Ostack waitcostumava lerpresent == "yes"como o único sinal de prontidão, por isso um manifesto com qualquer Kind declarativo gastava todo o--timeoutà espera de um marcador que esse Kind nunca pode produzir.
Os verbos genéricos e o drift#
O delonix get/describe/delete <plural> roteia por Kind através de três listas em
bins/delonix-runtime-bin/src/cmd/verbs.rs — GET_ROUTES, DESCRIBE_ROUTES e
DELETE_ROUTES — mais um braço por verbo que chama o próprio cmd_ls, cmd_describe e
remove_for_replace do Kind. Um Kind que não lhes consiga responder escreve o obstáculo em
no_verb_reason (o Stack é lido de um ficheiro, o Workload baixa no load, …). Repara no que o
gate faz e não faz: o a_kind_never_both_routes_and_claims_it_cannot falha só quando um Kind
ao mesmo tempo roteia e afirma que não pode; um Kind que não está em nenhuma das listas só produz
uma linha not wired yet: … no stderr durante a corrida do teste, e o delonix get <plural>
responde "not wired yet" ao utilizador. Lê essa linha; o build não te vai parar.
O delonix drift compara o carimbo last-applied com o que a máquina guarda, e a maioria dos
Kinds pode ser enumerada a partir do seu próprio store. Se o teu actual() precisar dos
documentos analisados para responder — o nó não guarda registo nenhum que liste o Kind por si só,
tal como um NetworkPolicy vive como regras nft num alvo — acrescenta-o a DOC_SCOPED em
bins/delonix-runtime-bin/src/cmd/drift.rs. O doc_scoped_matches_the_stack_wiring lê o
stack.rs (o sítio de chamada e a assinatura do actual) e falha se a lista e a fiação
discordarem; um módulo que recebe docs e os ignora (fn actual(_docs: …)) não pertence à lista.
Namespaces e completação#
Se o Kind é namespaced (namespaced != Namespaced::Never), precisa também de uma entrada em
NAMESPACE_SOURCES (bins/delonix-runtime-bin/src/cmd/complete.rs) — ou NsSource::Store(fn) a
ler o próprio store do Kind, ou NsSource::Via("OutroKind — razão") quando o namespace viaja
para o registo de outro Kind (o namespace de um Pod está nos seus Containers membros, por
exemplo). O every_namespaced_kind_declares_a_source falha caso contrário — o efeito prático de
faltar isto é a completação da shell nunca poder oferecer um inquilino cujo único recurso seja o
Kind novo.
A checklist#
Para um Kind que se comporta como o Service (primário, converge, tem teardown, namespaced):
kinds.rs— umpub constcom o nome, e uma linhaKindFacts, incluindo o seustack_group(ou uma entrada emstack_group_absent_reason).- Uma struct de spec com
JsonSchema; um braço emTYPED_KINDSe emmanifest_schema(schema.rs) — ou uma entrada emuntyped_hinta explicar porque não. manifest.rs— um braço emfilled_spec(spec_with_defaults), um braço emspec_fields_for, e uma amostra emstack_group_sample; o grupo emexamples/stack.yaml.stack.rs— braços emdesired_of,actual_of,converge_and_stamp,stamp_all,destroy_one,presence, e uma camada emrun_layersa chamar o próprioapply(docs)do Kind.reconcile.rs— uma entrada emhot_fieldsa nomear os campos que convergem ao vivo.- Se o Kind não converge ou não pode ser desfeito: uma frase específica em
not_converged_reason/no_teardown_reason(stack.rs). verbs.rs— o Kind emGET_ROUTES/DESCRIBE_ROUTES/DELETE_ROUTEScom os seus braços, ou uma razão emno_verb_reason.drift.rs—DOC_SCOPED, só se oactual()precisar mesmo dos documentos.- Se namespaced: uma entrada em
NAMESPACE_SOURCES(complete.rs). - Toda string nova visível ao utilizador em inglês no código e traduzida no
bins/delonix-runtime-bin/data/pt.po; um código de erro novo no dicionárioDX-CDNN(crates/foundation/delonix-model/src/codes.rs) também precisa do seu texto em PT (every_dictionary_text_has_a_portuguese_translation). Correpython3 scripts/lang_ratchet.py. delonix manifest schema > docs/schema/v1/delonix.jsoncom o binário da tua árvore, depoispython3 docs/gen.py <esse binário>para a página de Kinds do site do utilizador acompanhar.cargo test -p delonix-runtime-bin -p delonix-stack— os testes nomeados acima são o que apanha um passo saltado, não um revisor a ler o diff a olho.
Um Kind que seja Sugar/Aggregate (reescrito ou expandido no load, como o Workload ou o
Stack) salta a maior parte disto: o um_kind_que_baixa_para_outro_nao_pertence_ao_ciclo_do_stack
exige in_stack: false e converges: false para essas formas, e o trabalho vive antes no
manifest::load, onde a reescrita acontece.
A seguir: Fluxo de contribuição — worktrees, alinhamento de versão, a regra de língua, quando escrever um ADR, e como enviar a mudança.