Ligar cada critério de aceitação à evidência certa
Uma entrega tem vários resultados: o build pode terminar, a imagem pode ser publicada, o deployment pode instalar recursos e uma verificação pode testar o comportamento. Antes de aprovar, identifica qual destes resultados demonstra cada requisito. No caso fictício desta aula, uma aplicação de reporting exige um contrato funcional válido e uma verificação em pré-produção. O build tolerou a falha do contrato e ficou verde; a verificação posterior também falhou. A conclusão útil para o comité é que a aceitação continua pendente. Regista a execução, a entrega candidata, o critério em falta, o responsável e a próxima ação. Não uses a cor agregada como substituto destes factos. Em Cloud Deploy, repetir uma verificação falhada inicia outra execução e pode colocar o rollout em IN_PROGRESS. Esse estado ainda não demonstra aprovação nem rollback concluído. A equipa APS deve saber qual é o serviço atualmente exposto e que evidência falta antes de preparar a passagem a produção.
Ler a configuração de falhas, dependências e volumes
A revisão de um pipeline deve responder a três perguntas separadas: o que bloqueia, o que espera e o que persiste? Uma etapa com allowFailure pode falhar sem tornar o resultado global uma falha. Para um teste obrigatório, revê a configuração e executa um teste negativo controlado que demonstre o bloqueio. Depois desenha as dependências por ID. No excerto da pergunta, waitFor com o marcador de início permite ao publicador arrancar sem esperar pelo contrato, apesar de aparecer depois no YAML. Finalmente, distingue ordem e partilha de dados: esperar por uma etapa não copia o seu /tmp para outro contentor. Se o relatório precisa de atravessar etapas do mesmo build, usa /workspace ou um volume partilhado configurado. Confirma os caminhos de escrita e leitura. As imagens example.invalid usadas no exercício são identificadores fictícios para leitura; não constituem um pipeline executável. Na revisão entre equipas, pede uma demonstração concreta de cada uma das três propriedades.
Distinguir configuração, estado e objetos remotos
Uma revisão Terraform deve manter três perspetivas: a intenção escrita na configuração, a representação no estado e o que existe no fornecedor. Uma alteração manual autorizada pode criar diferenças entre elas. Um plano refresh-only propõe reconciliar estado e outputs com alterações externas; não é um pedido para repor o remoto no valor antigo e não reescreve os ficheiros HCL. O comando plan, isoladamente, não aplica a proposta. Depois de decidir como registar a intervenção, revê a configuração pretendida e um plano normal antes de avançar. O mesmo cuidado aparece no import clássico: associar uma base existente ao estado não demonstra que o bloco resource manual representa todos os atributos relevantes. Pede ao responsável uma revisão das diferenças e das alterações que o provider pretende realizar. Este exercício não executa Terraform nem substitui um ensaio num ambiente autorizado. A decisão de projeto deve distinguir import concluído de adoção operacional segura e reproduzível.
Identificar quem autentica cada interação
O sucesso de um comando local não prova que uma aplicação usa a mesma identidade. Num portátil, a sessão gcloud e a configuração ADC são distintas. Se a biblioteca não encontra credenciais, confirma primeiro o mecanismo configurado, antes de pedir permissões adicionais. Num contexto de desenvolvimento autorizado, prepara ADC pelo método apropriado e verifica a identidade sem expor tokens em logs. Numa entrega por Cloud Tasks, separa também a criação da tarefa da autenticação do pedido ao destino. No cenário, o destino recebe um ID token com audience diferente da esperada; dar mais permissões ao produtor na fila não corrige essa claim. Alinha a configuração com o contrato do serviço e observa uma nova entrega. O laboratório desta aula não usa credenciais nem invoca serviços. Ao preparar o handover, documenta identidade, finalidade, destino e evidência de cada fronteira, incluindo o comportamento quando a autenticação falha. Assim, operações consegue investigar a etapa correta sem alargar acesso por tentativa.
Exercício: terminar uma pesquisa apenas quando acaba a continuação
O inventário fictício começa com uma página vazia e um token de continuação. A segunda página contém report-A e outro token; a terceira contém report-B e termina a sequência. Antes de executar, escreve os três pedidos esperados e a lista final. Corre python3 content/labs/pca-pagination-contract/run.py e compara a previsão com o resultado. O coletor usa um callback local, mantém parent, filter e page_size, e transmite tokens sem interpretar o seu conteúdo. Continua mesmo quando items está vazio. Os limites positivos de páginas e de page_size são escolhas defensivas deste cliente didático; não são todas as regras de uma API AIP-158, que também admite page_size zero com valor por omissão do serviço. O exercício conserva valores repetidos em vez de os eliminar silenciosamente. Percorrer páginas não garante um snapshot de uma coleção em mudança. Para uma decisão de desativação, a equipa ainda precisa de definir consistência, âmbito temporal e confirmação das dependências reais.
Tornar falhas parciais visíveis ao consumidor
Um script de inventário pode recolher alguns recursos e falhar na página seguinte. Se devolver a lista parcial como sucesso, o relatório passa a confundir ausência com falha de recolha. No exercício, erros do callback, respostas inválidas, tokens repetidos e esgotamento do limite provocam falha explícita; não devolvem um resultado parcial aprovado. Podes inspecionar a implementação e alterar a sequência para repetir um token anterior. Explica por que motivo terminar esse ciclo com uma lista vazia seria perigoso para um comité de desativação. O modelo copia pedidos e resultados para que uma alteração num dicionário do callback não mude o âmbito do pedido seguinte. Não implementa retries, gestão de quotas, expiração de credenciais ou consistência distribuída. Num sistema real, define limites de tempo, tratamento de erros e observabilidade próprios. Uma alternativa de produto é apresentar recolha incompleta de forma explícita, impedindo que seja interpretada como confirmação de inventário vazio.
Limitar updates à intenção e testar rejeições
Quando uma API documenta FieldMask, o payload e a máscara têm papéis diferentes. No recurso fictício, selecionar enabled e enviar false desativa esse campo; enviar também label=B fora da máscara não altera label=A. Formula o pedido a partir da intenção, não de todos os campos que um formulário conhece. Uma máscara wildcard com semântica de substituição completa pode atingir campos mutáveis que um cliente antigo desconhece. O teste deve confirmar tanto a alteração pretendida como a preservação dos restantes valores. Aplica o mesmo raciocínio aos controlos de acesso: executar VerifyAPIKey não prova rejeição de uma chave inválida se continueOnError permite prosseguir e não existe outro bloqueio. Para cada controlo obrigatório, prepara um teste negativo com resultado esperado e ligação ao requisito. Os exemplos são contratos explicitamente definidos para o exercício; não assumas que qualquer API ou qualquer proxy tem essas configurações. Consulta a documentação e a configuração efetiva antes de generalizar.
Fechar a entrega com uma decisão que RUN consegue sustentar
Na simulação final, apresenta ao sponsor um registo curto: contrato falhado, configuração que tolera essa falha, verificação de pré-produção falhada e condições para nova avaliação. Propõe manter o serviço atual e replanear a janela enquanto os responsáveis investigam. Se o negócio pedir redução de âmbito, descreve exatamente o critério alterado, a autorização necessária e as limitações da nova decisão; não mantenhas a declaração de cumprimento total. Pede depois a um colega de operações que encontre a execução candidata, explique a dependência entre etapas, localize o relatório e distinga retry em curso de aceitação. Esse exercício testa a utilidade do handover. Como resumo, conserva cinco fronteiras: resultado agregado versus requisito, sequência versus persistência, estado versus configuração, autenticação do produtor versus destino e resposta parcial versus recolha concluída. Os tópicos relacionados são reconciliação de dados, observabilidade e governação de mudanças. A aprendizagem é conseguir justificar a decisão com evidência ligada ao requisito, sem prometer o que o ensaio não mediu.
"""Original local exercise. No network, credentials, SDK or snapshot guarantee."""
from copy import deepcopy
import json
def collect(fetch, parent, query_filter, page_size=20, max_pages=100):
"""Traverse a synthetic List contract; fail instead of returning partial success."""
if not isinstance(parent, str) or not parent.strip():
raise ValueError('parent must be nonempty text')
if not isinstance(query_filter, str):
raise ValueError('filter must be text')
for value, label in [(page_size, 'page_size'), (max_pages, 'max_pages')]:
if type(value) is not int or not 1 <= value <= 1000:
raise ValueError(label + ' must be an integer from 1 to 1000')
base = {'parent': parent, 'filter': query_filter, 'page_size': page_size}
token, seen, items = '', set(), []
for _ in range(max_pages):
request = deepcopy(base)
request['page_token'] = token
response = fetch(request)
if not isinstance(response, dict) or not isinstance(response.get('items'), list):
raise ValueError('response must contain an items list')
if len(response['items']) > page_size:
raise ValueError('synthetic response exceeds requested page size')
if any(not isinstance(item, dict) for item in response['items']):
raise ValueError('synthetic items must be dictionaries')
next_token = response.get('next_page_token', '')
if not isinstance(next_token, str):
raise ValueError('continuation token must be text')
items.extend(deepcopy(response['items']))
if not next_token:
return items
if next_token in seen:
raise ValueError('repeated continuation token')
seen.add(next_token)
token = next_token
raise ValueError('page bound reached before completion')
def scripted(pages):
"""Original in-memory fixture with exact opaque-token lookup."""
calls = []
def fetch(request):
calls.append(deepcopy(request))
return deepcopy(pages[request['page_token']])
return fetch, calls
def main():
names = []
def check(name, condition):
if not condition:
raise AssertionError(name)
names.append(name)
def rejects(name, callback, kind=ValueError):
try:
callback()
except kind:
names.append(name)
else:
raise AssertionError(name)
pages = {
'': {'items': [], 'next_page_token': 'opaque_x7'},
'opaque_x7': {'items': [{'id': 'report-A'}], 'next_page_token': 'opaque_Z2'},
'opaque_Z2': {'items': [{'id': 'report-B'}]}
}
original = deepcopy(pages)
fetch, calls = scripted(pages)
result = collect(fetch, 'projects/fixture', 'state:READY')
check('empty intermediate page is traversed', result == [{'id': 'report-A'}, {'id': 'report-B'}])
check('opaque tokens are passed unchanged', [c['page_token'] for c in calls] == ['', 'opaque_x7', 'opaque_Z2'])
check('parent remains stable', all(c['parent'] == 'projects/fixture' for c in calls))
check('filter remains stable', all(c['filter'] == 'state:READY' for c in calls))
check('page size remains stable by client choice', all(c['page_size'] == 20 for c in calls))
check('input fixtures are not mutated', pages == original)
check('missing continuation ends traversal', len(calls) == 3)
f, c = scripted({'': {'items': [], 'next_page_token': ''}})
check('empty final collection is accepted', collect(f, 'p', '') == [] and len(c) == 1)
f, _ = scripted({'': {'items': [{'id': 'same'}], 'next_page_token': 'a'}, 'a': {'items': [{'id': 'same'}]}})
check('repeated resource values are retained', collect(f, 'p', '') == [{'id': 'same'}, {'id': 'same'}])
f, _ = scripted({'': {'items': [], 'next_page_token': 'a'}, 'a': {'items': [], 'next_page_token': 'a'}})
rejects('self-repeating continuation fails', lambda: collect(f, 'p', ''))
f, _ = scripted({'': {'items': [], 'next_page_token': 'a'}, 'a': {'items': [], 'next_page_token': 'b'}, 'b': {'items': [], 'next_page_token': 'a'}})
rejects('multi-token cycle fails', lambda: collect(f, 'p', ''))
f, c = scripted(pages)
rejects('page budget does not return partial success', lambda: collect(f, 'p', '', max_pages=2))
check('page budget limits callback invocations', len(c) == 2)
f, _ = scripted(pages)
check('completion on exact page bound succeeds', len(collect(f, 'p', '', max_pages=3)) == 2)
for value, label in [(0, 'zero'), (-1, 'negative'), (True, 'boolean'), (2.5, 'fraction'), (1001, 'above limit')]:
rejects('reject ' + label + ' page bound', lambda v=value: collect(lambda _: {}, 'p', '', max_pages=v))
rejects('reject boolean page size', lambda: collect(lambda _: {}, 'p', '', page_size=True))
rejects('reject zero page size in this client', lambda: collect(lambda _: {}, 'p', '', page_size=0))
rejects('reject blank parent', lambda: collect(lambda _: {}, ' ', ''))
rejects('reject nontext filter', lambda: collect(lambda _: {}, 'p', None))
for response, label in [(None, 'nonobject response'), ({}, 'missing items'), ({'items': {}}, 'nonlist items'), ({'items': ['A']}, 'nonobject item'), ({'items': [], 'next_page_token': None}, 'nontext token')]:
rejects('reject ' + label, lambda r=response: collect(lambda _: r, 'p', ''))
rejects('reject oversized synthetic page', lambda: collect(lambda _: {'items': [{}, {}]}, 'p', '', page_size=1))
def fails_second(request):
if request['page_token']:
raise RuntimeError('fixture transport failure')
return {'items': [{'id': 'partial'}], 'next_page_token': 'later'}
rejects('callback failure does not return partial data', lambda: collect(fails_second, 'p', ''), RuntimeError)
mutation_calls = []
def mutates_request(request):
mutation_calls.append(deepcopy(request))
token = request['page_token']
request.update(parent='changed', filter='changed', page_size=1)
return {'items': [], **({'next_page_token': 'a'} if not token else {})}
collect(mutates_request, 'original', 'ready', page_size=20)
check('callback mutation cannot change next request scope', mutation_calls[1] == {'parent': 'original', 'filter': 'ready', 'page_size': 20, 'page_token': 'a'})
external = {'items': [{'nested': {'value': 1}}]}
output = collect(lambda _: external, 'p', '')
output[0]['nested']['value'] = 9
check('output nested objects are isolated', external['items'][0]['nested']['value'] == 1)
print(json.dumps({'passed': len(names), 'checks': names, 'result': result, 'requests': calls, 'network': False, 'vendorExecution': False, 'persistentWrites': False}))
if __name__ == '__main__':
main()
Um build verde tolerou um teste obrigatório falhado; uma verificação posterior também falhou. A equipa precisa de nova evidência antes de aprovar.
Armadilhas comuns
Confundir cor verde com aceitação, ordem com partilha de ficheiros, refresh com reversão, login CLI com ADC e página vazia com fim da pesquisa.
Tópicos relacionados: Reconciliação e inventários completos · Governação de mudanças e passagem a RUN · Identidades de execução e observabilidade
Liga cada conclusão à evidência que a suporta e testa explicitamente as falhas que devem impedir a aprovação.
Referência: Build configuration file schema · Current linked standard guide; edition date unconfirmed (2026-09-30 inspection)