korvid 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- korvid-0.1.2/.claude/hooks/protect-files.sh +11 -0
- korvid-0.1.2/.claude/settings.json +37 -0
- korvid-0.1.2/.github/dependabot.yml +76 -0
- korvid-0.1.2/.github/scripts/wait-aks-settled.sh +76 -0
- korvid-0.1.2/.github/workflows/ci.yml +160 -0
- korvid-0.1.2/.github/workflows/codeql.yml +33 -0
- korvid-0.1.2/.github/workflows/k8s-contract.yml +286 -0
- korvid-0.1.2/.github/workflows/release.yml +524 -0
- korvid-0.1.2/.gitignore +12 -0
- korvid-0.1.2/.pre-commit-config.yaml +46 -0
- korvid-0.1.2/AGENTS.md +141 -0
- korvid-0.1.2/CLAUDE.md +1 -0
- korvid-0.1.2/LICENSE +202 -0
- korvid-0.1.2/Makefile +16 -0
- korvid-0.1.2/PKG-INFO +292 -0
- korvid-0.1.2/README.md +240 -0
- korvid-0.1.2/SECURITY.md +48 -0
- korvid-0.1.2/deploy/eval/ollama.yaml +100 -0
- korvid-0.1.2/docs/agent.md +482 -0
- korvid-0.1.2/docs/airgap.md +158 -0
- korvid-0.1.2/docs/assets/demo.gif +0 -0
- korvid-0.1.2/docs/demo/README.md +26 -0
- korvid-0.1.2/docs/demo/demo.py +310 -0
- korvid-0.1.2/docs/demo/demo.tape +68 -0
- korvid-0.1.2/docs/dev/README.md +28 -0
- korvid-0.1.2/docs/dev/agent-decisions.md +279 -0
- korvid-0.1.2/docs/dev/contract-tests.md +122 -0
- korvid-0.1.2/docs/dev/plans/2026-07-24-phase1-foundation.md +1790 -0
- korvid-0.1.2/docs/dev/plans/2026-07-24-plan23-logs-universal-resources.md +355 -0
- korvid-0.1.2/docs/dev/plans/2026-07-24-plan4-agent-runtime-slice1.md +608 -0
- korvid-0.1.2/docs/dev/plans/2026-07-25-plan4-slice2-auth-setup.md +1012 -0
- korvid-0.1.2/docs/dev/plans/2026-07-25-plan4-slice3-ui-drive.md +92 -0
- korvid-0.1.2/docs/dev/quality-gates.md +76 -0
- korvid-0.1.2/docs/dev/specs/2026-07-23-korvid-tui-design.md +299 -0
- korvid-0.1.2/docs/dev/specs/2026-07-24-korvid-engineering-standards.md +242 -0
- korvid-0.1.2/docs/dev/specs/2026-08-04-local-model-aks-evaluation-design.md +181 -0
- korvid-0.1.2/docs/dev/specs/2026-08-06-large-cluster-performance-qualification-design.md +381 -0
- korvid-0.1.2/docs/dev/specs/2026-08-09-configurable-agent-prompts-design.md +347 -0
- korvid-0.1.2/docs/dev/ui-controllers.md +183 -0
- korvid-0.1.2/docs/evals/methodology.md +340 -0
- korvid-0.1.2/docs/evals/scenarios.md +79 -0
- korvid-0.1.2/docs/evals/scoreboard.md +318 -0
- korvid-0.1.2/docs/helm-operators.md +175 -0
- korvid-0.1.2/docs/keybindings.md +85 -0
- korvid-0.1.2/docs/mcp.md +182 -0
- korvid-0.1.2/docs/ops.md +259 -0
- korvid-0.1.2/docs/performance.md +114 -0
- korvid-0.1.2/docs/provider-plugins.md +425 -0
- korvid-0.1.2/docs/release-notes/v0.1.2.md +122 -0
- korvid-0.1.2/docs/release.md +291 -0
- korvid-0.1.2/docs/threat-model.md +329 -0
- korvid-0.1.2/docs/tui.md +156 -0
- korvid-0.1.2/docs/windows.md +48 -0
- korvid-0.1.2/pyproject.toml +159 -0
- korvid-0.1.2/scripts/release/build-constraints.in +2 -0
- korvid-0.1.2/scripts/release/build-constraints.txt +15 -0
- korvid-0.1.2/scripts/release/bundle.py +119 -0
- korvid-0.1.2/scripts/release/check_artifacts.py +190 -0
- korvid-0.1.2/scripts/release/check_dry_run.py +81 -0
- korvid-0.1.2/scripts/release/check_sbom.py +56 -0
- korvid-0.1.2/scripts/release/check_source.py +63 -0
- korvid-0.1.2/scripts/release/check_version.py +45 -0
- korvid-0.1.2/scripts/release/compare_assets.py +42 -0
- korvid-0.1.2/scripts/release/metadata.py +41 -0
- korvid-0.1.2/scripts/release/offline_verify.py +203 -0
- korvid-0.1.2/scripts/release/release_manifest.py +43 -0
- korvid-0.1.2/scripts/release/smoke_install.py +439 -0
- korvid-0.1.2/scripts/release/version_format.py +23 -0
- korvid-0.1.2/src/korvid/__init__.py +3 -0
- korvid-0.1.2/src/korvid/__main__.py +1233 -0
- korvid-0.1.2/src/korvid/agent/__init__.py +0 -0
- korvid-0.1.2/src/korvid/agent/context.py +61 -0
- korvid-0.1.2/src/korvid/agent/credentials.py +16 -0
- korvid-0.1.2/src/korvid/agent/events.py +66 -0
- korvid-0.1.2/src/korvid/agent/evidence.py +228 -0
- korvid-0.1.2/src/korvid/agent/navigation.py +125 -0
- korvid-0.1.2/src/korvid/agent/outbound.py +916 -0
- korvid-0.1.2/src/korvid/agent/profiles.py +231 -0
- korvid-0.1.2/src/korvid/agent/prompts.py +203 -0
- korvid-0.1.2/src/korvid/agent/provider.py +91 -0
- korvid-0.1.2/src/korvid/agent/provider_plugin.py +302 -0
- korvid-0.1.2/src/korvid/agent/runtime.py +1131 -0
- korvid-0.1.2/src/korvid/agent/setup.py +67 -0
- korvid-0.1.2/src/korvid/cli.py +26 -0
- korvid-0.1.2/src/korvid/core/__init__.py +0 -0
- korvid-0.1.2/src/korvid/core/audit.py +325 -0
- korvid-0.1.2/src/korvid/core/config.py +881 -0
- korvid-0.1.2/src/korvid/core/debugimage.py +287 -0
- korvid-0.1.2/src/korvid/core/errors.py +14 -0
- korvid-0.1.2/src/korvid/core/filters.py +278 -0
- korvid-0.1.2/src/korvid/core/findings.py +113 -0
- korvid-0.1.2/src/korvid/core/keybindings.py +171 -0
- korvid-0.1.2/src/korvid/core/logbuffer.py +82 -0
- korvid-0.1.2/src/korvid/core/logexport.py +82 -0
- korvid-0.1.2/src/korvid/core/mcp.py +49 -0
- korvid-0.1.2/src/korvid/core/portforward.py +824 -0
- korvid-0.1.2/src/korvid/core/private_export.py +45 -0
- korvid-0.1.2/src/korvid/core/pvc_analysis.py +635 -0
- korvid-0.1.2/src/korvid/core/redaction.py +480 -0
- korvid-0.1.2/src/korvid/core/secrets.py +104 -0
- korvid-0.1.2/src/korvid/core/service_analysis.py +242 -0
- korvid-0.1.2/src/korvid/core/sorting.py +122 -0
- korvid-0.1.2/src/korvid/core/store.py +76 -0
- korvid-0.1.2/src/korvid/core/transfer.py +648 -0
- korvid-0.1.2/src/korvid/core/watch.py +140 -0
- korvid-0.1.2/src/korvid/evals/__init__.py +6 -0
- korvid-0.1.2/src/korvid/evals/__main__.py +554 -0
- korvid-0.1.2/src/korvid/evals/fake_kube.py +253 -0
- korvid-0.1.2/src/korvid/evals/grader.py +511 -0
- korvid-0.1.2/src/korvid/evals/journey.py +222 -0
- korvid-0.1.2/src/korvid/evals/journey_runner.py +432 -0
- korvid-0.1.2/src/korvid/evals/journeys/compare-namespaces.yaml +202 -0
- korvid-0.1.2/src/korvid/evals/journeys/healthy-stop.yaml +77 -0
- korvid-0.1.2/src/korvid/evals/journeys/logs-to-events.yaml +67 -0
- korvid-0.1.2/src/korvid/evals/journeys/namespace-triage.yaml +245 -0
- korvid-0.1.2/src/korvid/evals/journeys/rbac-evidence-gap.yaml +152 -0
- korvid-0.1.2/src/korvid/evals/journeys/rollout-owner-chain.yaml +245 -0
- korvid-0.1.2/src/korvid/evals/journeys/triage-and-correct.yaml +204 -0
- korvid-0.1.2/src/korvid/evals/journeys/tui-follow.yaml +108 -0
- korvid-0.1.2/src/korvid/evals/journeys_cli.py +203 -0
- korvid-0.1.2/src/korvid/evals/live_journey.py +237 -0
- korvid-0.1.2/src/korvid/evals/live_journeys/triage-and-correct.yaml +55 -0
- korvid-0.1.2/src/korvid/evals/runner.py +503 -0
- korvid-0.1.2/src/korvid/evals/scenario.py +287 -0
- korvid-0.1.2/src/korvid/evals/scenarios/bad-command-crash.yaml +56 -0
- korvid-0.1.2/src/korvid/evals/scenarios/crashloop-app-panic.yaml +83 -0
- korvid-0.1.2/src/korvid/evals/scenarios/crashloop-dependency-unreachable.yaml +65 -0
- korvid-0.1.2/src/korvid/evals/scenarios/crashloop-missing-env.yaml +62 -0
- korvid-0.1.2/src/korvid/evals/scenarios/healthy-deployment.yaml +118 -0
- korvid-0.1.2/src/korvid/evals/scenarios/healthy-restart-history.yaml +62 -0
- korvid-0.1.2/src/korvid/evals/scenarios/healthy-service-endpoints.yaml +111 -0
- korvid-0.1.2/src/korvid/evals/scenarios/image-pull-auth.yaml +67 -0
- korvid-0.1.2/src/korvid/evals/scenarios/image-pull-typo.yaml +61 -0
- korvid-0.1.2/src/korvid/evals/scenarios/init-container-failing.yaml +71 -0
- korvid-0.1.2/src/korvid/evals/scenarios/job-backoff-limit-exceeded.yaml +76 -0
- korvid-0.1.2/src/korvid/evals/scenarios/liveness-probe-failing.yaml +70 -0
- korvid-0.1.2/src/korvid/evals/scenarios/missing-configmap-mount.yaml +58 -0
- korvid-0.1.2/src/korvid/evals/scenarios/missing-secret-env.yaml +57 -0
- korvid-0.1.2/src/korvid/evals/scenarios/node-pressure-eviction.yaml +49 -0
- korvid-0.1.2/src/korvid/evals/scenarios/oom-killed.yaml +70 -0
- korvid-0.1.2/src/korvid/evals/scenarios/pending-insufficient-cpu.yaml +42 -0
- korvid-0.1.2/src/korvid/evals/scenarios/pending-node-selector.yaml +44 -0
- korvid-0.1.2/src/korvid/evals/scenarios/pvc-pending-no-storageclass.yaml +70 -0
- korvid-0.1.2/src/korvid/evals/scenarios/pvc-wait-for-first-consumer.yaml +80 -0
- korvid-0.1.2/src/korvid/evals/scenarios/quota-blocked-scheduling.yaml +77 -0
- korvid-0.1.2/src/korvid/evals/scenarios/readiness-probe-failing.yaml +63 -0
- korvid-0.1.2/src/korvid/evals/scenarios/service-endpoints-not-ready.yaml +123 -0
- korvid-0.1.2/src/korvid/evals/scenarios/service-selector-mismatch.yaml +79 -0
- korvid-0.1.2/src/korvid/evals/scenarios/stuck-rollout.yaml +148 -0
- korvid-0.1.2/src/korvid/evals/scripted.py +43 -0
- korvid-0.1.2/src/korvid/evals/serving.py +196 -0
- korvid-0.1.2/src/korvid/k8s/__init__.py +0 -0
- korvid-0.1.2/src/korvid/k8s/client.py +1768 -0
- korvid-0.1.2/src/korvid/k8s/columns.py +119 -0
- korvid-0.1.2/src/korvid/k8s/components.py +218 -0
- korvid-0.1.2/src/korvid/k8s/csp.py +99 -0
- korvid-0.1.2/src/korvid/k8s/discovery.py +42 -0
- korvid-0.1.2/src/korvid/k8s/drain.py +302 -0
- korvid-0.1.2/src/korvid/k8s/dryrun.py +106 -0
- korvid-0.1.2/src/korvid/k8s/errors.py +29 -0
- korvid-0.1.2/src/korvid/k8s/helm.py +281 -0
- korvid-0.1.2/src/korvid/k8s/helmcli.py +475 -0
- korvid-0.1.2/src/korvid/k8s/logs.py +21 -0
- korvid-0.1.2/src/korvid/k8s/managed.py +159 -0
- korvid-0.1.2/src/korvid/k8s/metrics.py +177 -0
- korvid-0.1.2/src/korvid/k8s/models.py +952 -0
- korvid-0.1.2/src/korvid/k8s/olm.py +180 -0
- korvid-0.1.2/src/korvid/k8s/portforward.py +84 -0
- korvid-0.1.2/src/korvid/k8s/reads.py +61 -0
- korvid-0.1.2/src/korvid/k8s/relations.py +33 -0
- korvid-0.1.2/src/korvid/k8s/telemetry.py +19 -0
- korvid-0.1.2/src/korvid/k8s/telepresence.py +272 -0
- korvid-0.1.2/src/korvid/k8s/writes.py +201 -0
- korvid-0.1.2/src/korvid/mcp/__init__.py +0 -0
- korvid-0.1.2/src/korvid/mcp/server.py +626 -0
- korvid-0.1.2/src/korvid/providers/__init__.py +0 -0
- korvid-0.1.2/src/korvid/providers/configurator.py +211 -0
- korvid-0.1.2/src/korvid/providers/entra.py +56 -0
- korvid-0.1.2/src/korvid/providers/github_copilot.py +163 -0
- korvid-0.1.2/src/korvid/providers/net.py +89 -0
- korvid-0.1.2/src/korvid/providers/ollama.py +321 -0
- korvid-0.1.2/src/korvid/providers/openai_compat.py +171 -0
- korvid-0.1.2/src/korvid/providers/plugin_registry.py +355 -0
- korvid-0.1.2/src/korvid/providers/registry.py +253 -0
- korvid-0.1.2/src/korvid/providers/static_creds.py +23 -0
- korvid-0.1.2/src/korvid/providers/token_store.py +133 -0
- korvid-0.1.2/src/korvid/py.typed +0 -0
- korvid-0.1.2/src/korvid/tools/__init__.py +0 -0
- korvid-0.1.2/src/korvid/tools/diagnose.py +348 -0
- korvid-0.1.2/src/korvid/tools/executor.py +1895 -0
- korvid-0.1.2/src/korvid/tools/follow.py +164 -0
- korvid-0.1.2/src/korvid/tools/proposals.py +400 -0
- korvid-0.1.2/src/korvid/tools/registry.py +1189 -0
- korvid-0.1.2/src/korvid/tools/structured.py +282 -0
- korvid-0.1.2/src/korvid/ui/__init__.py +0 -0
- korvid-0.1.2/src/korvid/ui/app.py +8060 -0
- korvid-0.1.2/src/korvid/ui/command.py +139 -0
- korvid-0.1.2/src/korvid/ui/debug.py +271 -0
- korvid-0.1.2/src/korvid/ui/drain.py +273 -0
- korvid-0.1.2/src/korvid/ui/forward_controller.py +678 -0
- korvid-0.1.2/src/korvid/ui/helm_controller.py +759 -0
- korvid-0.1.2/src/korvid/ui/hints.py +334 -0
- korvid-0.1.2/src/korvid/ui/logformat.py +119 -0
- korvid-0.1.2/src/korvid/ui/messages.py +140 -0
- korvid-0.1.2/src/korvid/ui/navigation.py +67 -0
- korvid-0.1.2/src/korvid/ui/operator_controller.py +642 -0
- korvid-0.1.2/src/korvid/ui/shell.py +208 -0
- korvid-0.1.2/src/korvid/ui/shell_controller.py +939 -0
- korvid-0.1.2/src/korvid/ui/theme.py +91 -0
- korvid-0.1.2/src/korvid/ui/transfer.py +237 -0
- korvid-0.1.2/src/korvid/ui/ui_surface.py +136 -0
- korvid-0.1.2/src/korvid/ui/view_state.py +99 -0
- korvid-0.1.2/src/korvid/ui/widgets/__init__.py +0 -0
- korvid-0.1.2/src/korvid/ui/widgets/agent_panel.py +483 -0
- korvid-0.1.2/src/korvid/ui/widgets/agent_setup_screen.py +445 -0
- korvid-0.1.2/src/korvid/ui/widgets/command_bar.py +87 -0
- korvid-0.1.2/src/korvid/ui/widgets/confirm_screen.py +306 -0
- korvid-0.1.2/src/korvid/ui/widgets/containers_screen.py +131 -0
- korvid-0.1.2/src/korvid/ui/widgets/describe_screen.py +493 -0
- korvid-0.1.2/src/korvid/ui/widgets/filter_bar.py +46 -0
- korvid-0.1.2/src/korvid/ui/widgets/helm_chart_search.py +176 -0
- korvid-0.1.2/src/korvid/ui/widgets/helm_install.py +410 -0
- korvid-0.1.2/src/korvid/ui/widgets/helm_repos.py +230 -0
- korvid-0.1.2/src/korvid/ui/widgets/help_screen.py +211 -0
- korvid-0.1.2/src/korvid/ui/widgets/hierarchy_screen.py +363 -0
- korvid-0.1.2/src/korvid/ui/widgets/hint_detail.py +164 -0
- korvid-0.1.2/src/korvid/ui/widgets/hint_strip.py +179 -0
- korvid-0.1.2/src/korvid/ui/widgets/log_pane.py +439 -0
- korvid-0.1.2/src/korvid/ui/widgets/logo.py +63 -0
- korvid-0.1.2/src/korvid/ui/widgets/namespace_picker.py +33 -0
- korvid-0.1.2/src/korvid/ui/widgets/operator_install.py +210 -0
- korvid-0.1.2/src/korvid/ui/widgets/path_picker.py +258 -0
- korvid-0.1.2/src/korvid/ui/widgets/payload_inspector.py +76 -0
- korvid-0.1.2/src/korvid/ui/widgets/pick_screen.py +87 -0
- korvid-0.1.2/src/korvid/ui/widgets/port_forward_screen.py +332 -0
- korvid-0.1.2/src/korvid/ui/widgets/resize_prompt.py +171 -0
- korvid-0.1.2/src/korvid/ui/widgets/resource_table.py +1079 -0
- korvid-0.1.2/src/korvid/ui/widgets/secret_screen.py +230 -0
- korvid-0.1.2/src/korvid/ui/widgets/status_bar.py +112 -0
- korvid-0.1.2/src/korvid/ui/widgets/telepresence_screen.py +115 -0
- korvid-0.1.2/src/korvid/ui/widgets/top_bar.py +257 -0
- korvid-0.1.2/src/korvid/ui/widgets/transfer_screen.py +260 -0
- korvid-0.1.2/src/korvid/ui/write_gate.py +278 -0
- korvid-0.1.2/tach.toml +33 -0
- korvid-0.1.2/tests/__init__.py +0 -0
- korvid-0.1.2/tests/agent/__init__.py +0 -0
- korvid-0.1.2/tests/agent/test_context.py +47 -0
- korvid-0.1.2/tests/agent/test_credentials.py +18 -0
- korvid-0.1.2/tests/agent/test_events.py +16 -0
- korvid-0.1.2/tests/agent/test_evidence.py +744 -0
- korvid-0.1.2/tests/agent/test_interrupt.py +237 -0
- korvid-0.1.2/tests/agent/test_navigation.py +193 -0
- korvid-0.1.2/tests/agent/test_outbound.py +1710 -0
- korvid-0.1.2/tests/agent/test_plugin_runtime.py +283 -0
- korvid-0.1.2/tests/agent/test_profiles.py +444 -0
- korvid-0.1.2/tests/agent/test_provider.py +97 -0
- korvid-0.1.2/tests/agent/test_provider_plugin.py +907 -0
- korvid-0.1.2/tests/agent/test_runtime.py +3827 -0
- korvid-0.1.2/tests/contract/__init__.py +8 -0
- korvid-0.1.2/tests/contract/conftest.py +190 -0
- korvid-0.1.2/tests/contract/janitor.py +73 -0
- korvid-0.1.2/tests/contract/test_create_replace_contract.py +105 -0
- korvid-0.1.2/tests/contract/test_delete_contract.py +69 -0
- korvid-0.1.2/tests/contract/test_helm_contract.py +93 -0
- korvid-0.1.2/tests/contract/test_node_ops_contract.py +147 -0
- korvid-0.1.2/tests/contract/test_rbac_contract.py +143 -0
- korvid-0.1.2/tests/contract/test_resize_contract.py +68 -0
- korvid-0.1.2/tests/contract/test_scale_rollout_contract.py +99 -0
- korvid-0.1.2/tests/core/__init__.py +0 -0
- korvid-0.1.2/tests/core/test_audit.py +400 -0
- korvid-0.1.2/tests/core/test_config.py +1617 -0
- korvid-0.1.2/tests/core/test_debugimage.py +453 -0
- korvid-0.1.2/tests/core/test_errors.py +18 -0
- korvid-0.1.2/tests/core/test_filters.py +317 -0
- korvid-0.1.2/tests/core/test_keybindings.py +150 -0
- korvid-0.1.2/tests/core/test_logbuffer.py +120 -0
- korvid-0.1.2/tests/core/test_logexport.py +171 -0
- korvid-0.1.2/tests/core/test_portforward.py +1258 -0
- korvid-0.1.2/tests/core/test_private_export.py +76 -0
- korvid-0.1.2/tests/core/test_pvc_analysis.py +870 -0
- korvid-0.1.2/tests/core/test_redaction.py +713 -0
- korvid-0.1.2/tests/core/test_secrets.py +153 -0
- korvid-0.1.2/tests/core/test_service_analysis.py +269 -0
- korvid-0.1.2/tests/core/test_sorting.py +234 -0
- korvid-0.1.2/tests/core/test_store.py +146 -0
- korvid-0.1.2/tests/core/test_transfer.py +392 -0
- korvid-0.1.2/tests/core/test_transfer_stream.py +526 -0
- korvid-0.1.2/tests/core/test_watch.py +402 -0
- korvid-0.1.2/tests/evals/__init__.py +0 -0
- korvid-0.1.2/tests/evals/test_cli.py +403 -0
- korvid-0.1.2/tests/evals/test_fake_kube.py +533 -0
- korvid-0.1.2/tests/evals/test_grader.py +891 -0
- korvid-0.1.2/tests/evals/test_journey.py +779 -0
- korvid-0.1.2/tests/evals/test_journey_runner.py +407 -0
- korvid-0.1.2/tests/evals/test_journeys_cli.py +192 -0
- korvid-0.1.2/tests/evals/test_live_journey.py +174 -0
- korvid-0.1.2/tests/evals/test_runner.py +1017 -0
- korvid-0.1.2/tests/evals/test_scenario.py +279 -0
- korvid-0.1.2/tests/evals/test_scenarios_pack.py +301 -0
- korvid-0.1.2/tests/evals/test_serving.py +140 -0
- korvid-0.1.2/tests/evals/test_serving_manifest.py +50 -0
- korvid-0.1.2/tests/evals/test_tool_surface_arm.py +229 -0
- korvid-0.1.2/tests/fixtures/__init__.py +0 -0
- korvid-0.1.2/tests/fixtures/provider_plugin/__init__.py +0 -0
- korvid-0.1.2/tests/fixtures/provider_plugin/company_provider.py +80 -0
- korvid-0.1.2/tests/fixtures/provider_plugin/site_helpers.py +53 -0
- korvid-0.1.2/tests/fixtures/provider_plugin/unselected_provider.py +7 -0
- korvid-0.1.2/tests/k8s/__init__.py +0 -0
- korvid-0.1.2/tests/k8s/test_client.py +2353 -0
- korvid-0.1.2/tests/k8s/test_client_node_ops.py +281 -0
- korvid-0.1.2/tests/k8s/test_client_previews.py +374 -0
- korvid-0.1.2/tests/k8s/test_client_resize.py +169 -0
- korvid-0.1.2/tests/k8s/test_columns.py +123 -0
- korvid-0.1.2/tests/k8s/test_components.py +326 -0
- korvid-0.1.2/tests/k8s/test_csp.py +175 -0
- korvid-0.1.2/tests/k8s/test_discovery.py +196 -0
- korvid-0.1.2/tests/k8s/test_drain.py +388 -0
- korvid-0.1.2/tests/k8s/test_dryrun.py +93 -0
- korvid-0.1.2/tests/k8s/test_helm.py +549 -0
- korvid-0.1.2/tests/k8s/test_helmcli.py +836 -0
- korvid-0.1.2/tests/k8s/test_logs.py +298 -0
- korvid-0.1.2/tests/k8s/test_managed.py +267 -0
- korvid-0.1.2/tests/k8s/test_metrics.py +283 -0
- korvid-0.1.2/tests/k8s/test_models.py +1551 -0
- korvid-0.1.2/tests/k8s/test_models_trouble.py +394 -0
- korvid-0.1.2/tests/k8s/test_olm.py +180 -0
- korvid-0.1.2/tests/k8s/test_portforward.py +75 -0
- korvid-0.1.2/tests/k8s/test_relations.py +47 -0
- korvid-0.1.2/tests/k8s/test_telepresence.py +337 -0
- korvid-0.1.2/tests/mcp/__init__.py +0 -0
- korvid-0.1.2/tests/mcp/test_server.py +1141 -0
- korvid-0.1.2/tests/performance/__init__.py +0 -0
- korvid-0.1.2/tests/performance/cli.py +548 -0
- korvid-0.1.2/tests/performance/live.py +1612 -0
- korvid-0.1.2/tests/performance/manifests.py +147 -0
- korvid-0.1.2/tests/performance/metrics.py +816 -0
- korvid-0.1.2/tests/performance/profile.py +203 -0
- korvid-0.1.2/tests/performance/profiles/aks-1k.json +15 -0
- korvid-0.1.2/tests/performance/profiles/aks-live-1k.json +15 -0
- korvid-0.1.2/tests/performance/profiles/burst-50k.json +15 -0
- korvid-0.1.2/tests/performance/profiles/smoke-1k.json +13 -0
- korvid-0.1.2/tests/performance/profiles/steady-10k.json +13 -0
- korvid-0.1.2/tests/performance/replay.py +668 -0
- korvid-0.1.2/tests/performance/test_cli.py +998 -0
- korvid-0.1.2/tests/performance/test_live.py +2728 -0
- korvid-0.1.2/tests/performance/test_manifests.py +175 -0
- korvid-0.1.2/tests/performance/test_metrics.py +783 -0
- korvid-0.1.2/tests/performance/test_profile.py +173 -0
- korvid-0.1.2/tests/performance/test_replay.py +530 -0
- korvid-0.1.2/tests/performance/test_workload.py +63 -0
- korvid-0.1.2/tests/performance/workload.py +103 -0
- korvid-0.1.2/tests/platforms.py +47 -0
- korvid-0.1.2/tests/providers/__init__.py +0 -0
- korvid-0.1.2/tests/providers/test_configurator.py +513 -0
- korvid-0.1.2/tests/providers/test_entra.py +51 -0
- korvid-0.1.2/tests/providers/test_github_copilot.py +185 -0
- korvid-0.1.2/tests/providers/test_net.py +292 -0
- korvid-0.1.2/tests/providers/test_ollama.py +721 -0
- korvid-0.1.2/tests/providers/test_openai_compat.py +325 -0
- korvid-0.1.2/tests/providers/test_plugin_registry.py +1101 -0
- korvid-0.1.2/tests/providers/test_registry.py +870 -0
- korvid-0.1.2/tests/providers/test_token_store.py +320 -0
- korvid-0.1.2/tests/test_cli.py +121 -0
- korvid-0.1.2/tests/test_main_recovery.py +328 -0
- korvid-0.1.2/tests/test_main_wiring.py +1996 -0
- korvid-0.1.2/tests/test_optional_extras.py +166 -0
- korvid-0.1.2/tests/test_platforms.py +268 -0
- korvid-0.1.2/tests/test_release_scripts.py +1934 -0
- korvid-0.1.2/tests/test_sanity.py +16 -0
- korvid-0.1.2/tests/tools/__init__.py +0 -0
- korvid-0.1.2/tests/tools/test_diagnose.py +553 -0
- korvid-0.1.2/tests/tools/test_executor.py +4000 -0
- korvid-0.1.2/tests/tools/test_follow.py +178 -0
- korvid-0.1.2/tests/tools/test_list_resources.py +368 -0
- korvid-0.1.2/tests/tools/test_proposals.py +436 -0
- korvid-0.1.2/tests/tools/test_registry.py +602 -0
- korvid-0.1.2/tests/tools/test_structured.py +313 -0
- korvid-0.1.2/tests/tools/test_write_tools.py +251 -0
- korvid-0.1.2/tests/ui/__init__.py +0 -0
- korvid-0.1.2/tests/ui/test_adaptive_footer.py +487 -0
- korvid-0.1.2/tests/ui/test_agent_follow.py +203 -0
- korvid-0.1.2/tests/ui/test_agent_interrupt.py +517 -0
- korvid-0.1.2/tests/ui/test_agent_off.py +206 -0
- korvid-0.1.2/tests/ui/test_agent_panel.py +525 -0
- korvid-0.1.2/tests/ui/test_agent_setup_screen.py +648 -0
- korvid-0.1.2/tests/ui/test_agent_ui_drive.py +910 -0
- korvid-0.1.2/tests/ui/test_agent_wiring.py +937 -0
- korvid-0.1.2/tests/ui/test_agent_write.py +641 -0
- korvid-0.1.2/tests/ui/test_app.py +1187 -0
- korvid-0.1.2/tests/ui/test_busy_bird.py +183 -0
- korvid-0.1.2/tests/ui/test_column_sorting.py +574 -0
- korvid-0.1.2/tests/ui/test_command.py +261 -0
- korvid-0.1.2/tests/ui/test_confirm_screen.py +494 -0
- korvid-0.1.2/tests/ui/test_containers_screen.py +253 -0
- korvid-0.1.2/tests/ui/test_csp_footer.py +89 -0
- korvid-0.1.2/tests/ui/test_ctx_switch.py +1261 -0
- korvid-0.1.2/tests/ui/test_cursor_stability.py +141 -0
- korvid-0.1.2/tests/ui/test_custom_columns.py +221 -0
- korvid-0.1.2/tests/ui/test_debug_controller.py +218 -0
- korvid-0.1.2/tests/ui/test_describe.py +416 -0
- korvid-0.1.2/tests/ui/test_describe_search.py +374 -0
- korvid-0.1.2/tests/ui/test_drain_controller.py +215 -0
- korvid-0.1.2/tests/ui/test_drilldown.py +784 -0
- korvid-0.1.2/tests/ui/test_dryrun_preview.py +275 -0
- korvid-0.1.2/tests/ui/test_filter_enhancements.py +141 -0
- korvid-0.1.2/tests/ui/test_helm_actions.py +1649 -0
- korvid-0.1.2/tests/ui/test_helm_chart_search.py +233 -0
- korvid-0.1.2/tests/ui/test_helm_install_prompt.py +664 -0
- korvid-0.1.2/tests/ui/test_helm_repo_screen.py +283 -0
- korvid-0.1.2/tests/ui/test_helm_view.py +248 -0
- korvid-0.1.2/tests/ui/test_help_screen.py +324 -0
- korvid-0.1.2/tests/ui/test_hierarchy_nav.py +956 -0
- korvid-0.1.2/tests/ui/test_hierarchy_screen.py +405 -0
- korvid-0.1.2/tests/ui/test_hint_controller.py +211 -0
- korvid-0.1.2/tests/ui/test_hint_detail.py +119 -0
- korvid-0.1.2/tests/ui/test_hint_strip.py +234 -0
- korvid-0.1.2/tests/ui/test_hint_wiring.py +666 -0
- korvid-0.1.2/tests/ui/test_keybindings.py +176 -0
- korvid-0.1.2/tests/ui/test_log_pane.py +1616 -0
- korvid-0.1.2/tests/ui/test_logformat.py +225 -0
- korvid-0.1.2/tests/ui/test_mcp_follow.py +252 -0
- korvid-0.1.2/tests/ui/test_mcp_ui_context.py +304 -0
- korvid-0.1.2/tests/ui/test_messages.py +34 -0
- korvid-0.1.2/tests/ui/test_metrics_wiring.py +210 -0
- korvid-0.1.2/tests/ui/test_navigation.py +70 -0
- korvid-0.1.2/tests/ui/test_node_ops.py +738 -0
- korvid-0.1.2/tests/ui/test_node_shell.py +902 -0
- korvid-0.1.2/tests/ui/test_olm_view.py +716 -0
- korvid-0.1.2/tests/ui/test_operator_install.py +204 -0
- korvid-0.1.2/tests/ui/test_operator_uninstall.py +486 -0
- korvid-0.1.2/tests/ui/test_payload_inspector.py +182 -0
- korvid-0.1.2/tests/ui/test_pick_screen.py +58 -0
- korvid-0.1.2/tests/ui/test_port_forward.py +2113 -0
- korvid-0.1.2/tests/ui/test_proposals_ui.py +1283 -0
- korvid-0.1.2/tests/ui/test_protected_contexts.py +259 -0
- korvid-0.1.2/tests/ui/test_resize_flow.py +393 -0
- korvid-0.1.2/tests/ui/test_resize_prompt.py +207 -0
- korvid-0.1.2/tests/ui/test_resource_table_cells.py +353 -0
- korvid-0.1.2/tests/ui/test_secret_screen.py +479 -0
- korvid-0.1.2/tests/ui/test_shell.py +1504 -0
- korvid-0.1.2/tests/ui/test_shell_reservation.py +360 -0
- korvid-0.1.2/tests/ui/test_split_pane.py +757 -0
- korvid-0.1.2/tests/ui/test_table_column_widths.py +286 -0
- korvid-0.1.2/tests/ui/test_table_diff_update.py +269 -0
- korvid-0.1.2/tests/ui/test_table_row_memo.py +233 -0
- korvid-0.1.2/tests/ui/test_telepresence.py +354 -0
- korvid-0.1.2/tests/ui/test_theme.py +108 -0
- korvid-0.1.2/tests/ui/test_top_bar.py +352 -0
- korvid-0.1.2/tests/ui/test_transfer.py +655 -0
- korvid-0.1.2/tests/ui/test_transfer_controller.py +249 -0
- korvid-0.1.2/tests/ui/test_transfer_picker.py +808 -0
- korvid-0.1.2/tests/ui/test_view_state_seam.py +169 -0
- korvid-0.1.2/tests/ui/test_waits.py +65 -0
- korvid-0.1.2/tests/ui/test_widget_accessors.py +48 -0
- korvid-0.1.2/tests/ui/test_write_confirm_characterization.py +281 -0
- korvid-0.1.2/tests/ui/test_write_ops.py +1531 -0
- korvid-0.1.2/tests/ui/waits.py +49 -0
- korvid-0.1.2/uv.lock +2726 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
INPUT=$(cat)
|
|
3
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
|
4
|
+
PROTECTED=("uv.lock" ".github/workflows/" "tach.toml" ".pre-commit-config.yaml")
|
|
5
|
+
for pattern in "${PROTECTED[@]}"; do
|
|
6
|
+
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
|
|
7
|
+
echo "Blocked: $FILE_PATH is a protected gate file — ask the human to change it" >&2
|
|
8
|
+
exit 2
|
|
9
|
+
fi
|
|
10
|
+
done
|
|
11
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "file=$(jq -r '.tool_input.file_path // empty'); if echo \"$file\" | grep -q '\\.py$'; then uv run ruff check --fix --quiet \"$file\" 2>/dev/null; uv run ruff format --quiet \"$file\" 2>/dev/null; fi; exit 0"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"Stop": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "uv run pytest -x -q --tb=short 2>&1 | tail -20"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
updates:
|
|
4
|
+
# Python dependencies, resolved through uv.lock.
|
|
5
|
+
#
|
|
6
|
+
# Runtime vulnerabilities already fail CI: the `security` job audits the
|
|
7
|
+
# locked runtime resolution with pip-audit. Nothing audits the dev group,
|
|
8
|
+
# because dev packages do not ship in the wheel - but they do run in CI
|
|
9
|
+
# with the repository checked out, and nine GitPython advisories sat open
|
|
10
|
+
# because an alert with no path to a pull request is not a process.
|
|
11
|
+
- package-ecosystem: uv
|
|
12
|
+
directory: /
|
|
13
|
+
schedule:
|
|
14
|
+
interval: weekly
|
|
15
|
+
open-pull-requests-limit: 5
|
|
16
|
+
ignore:
|
|
17
|
+
# mcp 2.0 is a breaking release: the Server type parameters, the
|
|
18
|
+
# registration surface, `request_context`, the Tool schema field name
|
|
19
|
+
# and the streamable_http_client return arity all changed. That
|
|
20
|
+
# migration touches the path the approval gate sits on, so it is
|
|
21
|
+
# tracked as #260 rather than done as a bump. Remove this entry in
|
|
22
|
+
# the same change that closes it.
|
|
23
|
+
- dependency-name: mcp
|
|
24
|
+
update-types: [version-update:semver-major]
|
|
25
|
+
groups:
|
|
26
|
+
# One pull request per group keeps the review loop bounded: a routine
|
|
27
|
+
# dev-tool bump does not deserve the same attention as a runtime one.
|
|
28
|
+
#
|
|
29
|
+
# Grouped by name rather than by `dependency-type`: the uv updater
|
|
30
|
+
# reports every dependency as production, so a development group would
|
|
31
|
+
# match nothing and the runtime group would quietly absorb the dev
|
|
32
|
+
# tools it exists to separate. The runtime list is the project
|
|
33
|
+
# dependencies plus every extra - keep it in step with pyproject.toml.
|
|
34
|
+
runtime:
|
|
35
|
+
patterns:
|
|
36
|
+
- textual
|
|
37
|
+
- kubernetes_asyncio
|
|
38
|
+
- kubernetes-asyncio
|
|
39
|
+
- pyyaml
|
|
40
|
+
- rich
|
|
41
|
+
- regex
|
|
42
|
+
- httpx
|
|
43
|
+
- keyring
|
|
44
|
+
- mcp
|
|
45
|
+
- anyio
|
|
46
|
+
- starlette
|
|
47
|
+
- uvicorn
|
|
48
|
+
- azure-identity
|
|
49
|
+
dev-tooling:
|
|
50
|
+
patterns: ["*"]
|
|
51
|
+
exclude-patterns:
|
|
52
|
+
- textual
|
|
53
|
+
- kubernetes_asyncio
|
|
54
|
+
- kubernetes-asyncio
|
|
55
|
+
- pyyaml
|
|
56
|
+
- rich
|
|
57
|
+
- regex
|
|
58
|
+
- httpx
|
|
59
|
+
- keyring
|
|
60
|
+
- mcp
|
|
61
|
+
- anyio
|
|
62
|
+
- starlette
|
|
63
|
+
- uvicorn
|
|
64
|
+
- azure-identity
|
|
65
|
+
|
|
66
|
+
# Action pins. Every `uses:` in this repository is pinned to a commit SHA,
|
|
67
|
+
# which is what makes a supply-chain review meaningful and also what makes
|
|
68
|
+
# the pins invisible to a human reader - they cannot tell a current pin
|
|
69
|
+
# from a two-year-old one without looking each up.
|
|
70
|
+
- package-ecosystem: github-actions
|
|
71
|
+
directory: /
|
|
72
|
+
schedule:
|
|
73
|
+
interval: weekly
|
|
74
|
+
groups:
|
|
75
|
+
actions:
|
|
76
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wait until the AKS cluster has no operation in flight.
|
|
3
|
+
#
|
|
4
|
+
# Azure flips `powerState.code` as soon as it accepts a start/stop request,
|
|
5
|
+
# while `provisioningState` stays Starting/Stopping/Updating until the
|
|
6
|
+
# operation actually completes. Any other operation issued in that window is
|
|
7
|
+
# rejected with "there's an in-progress ... operation", so both the start and
|
|
8
|
+
# the stop paths have to wait for the real thing.
|
|
9
|
+
#
|
|
10
|
+
# Usage: wait-aks-settled.sh <resource-group> <cluster> [expected-power-state]
|
|
11
|
+
#
|
|
12
|
+
# Exits 0 once provisioningState is Succeeded (and, when given, powerState
|
|
13
|
+
# matches). Exits 1 if that has not happened within WAIT_AKS_TIMEOUT seconds
|
|
14
|
+
# (default 600), printing the last observed pair.
|
|
15
|
+
#
|
|
16
|
+
# Query failures are retried through the whole budget by default, because the
|
|
17
|
+
# caller after a stop is a barrier: giving up early would release the
|
|
18
|
+
# workflow's concurrency group while the stop is still in flight, which is the
|
|
19
|
+
# collision this exists to prevent. Set WAIT_AKS_MAX_QUERY_FAILURES to abort
|
|
20
|
+
# after N consecutive failures instead - only appropriate for an advisory
|
|
21
|
+
# pre-flight check whose failure does not skip anything.
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
group=${1:?resource group required}
|
|
25
|
+
cluster=${2:?cluster name required}
|
|
26
|
+
want_power=${3:-}
|
|
27
|
+
max_fails=${WAIT_AKS_MAX_QUERY_FAILURES:-0}
|
|
28
|
+
|
|
29
|
+
deadline=$(( SECONDS + ${WAIT_AKS_TIMEOUT:-600} ))
|
|
30
|
+
power=""
|
|
31
|
+
prov=""
|
|
32
|
+
fails=0
|
|
33
|
+
err_file=$(mktemp)
|
|
34
|
+
trap 'rm -f "$err_file"' EXIT
|
|
35
|
+
|
|
36
|
+
while :; do
|
|
37
|
+
if out=$(az aks show -g "$group" -n "$cluster" \
|
|
38
|
+
--query '[powerState.code,provisioningState]' -o tsv 2>"$err_file"); then
|
|
39
|
+
fails=0
|
|
40
|
+
read -r power prov < <(printf '%s' "$out" | tr '\n' ' ') || true
|
|
41
|
+
else
|
|
42
|
+
# An expired login, a wrong subscription or a deleted cluster is
|
|
43
|
+
# permanent, so every failure is logged rather than hidden. Whether to
|
|
44
|
+
# give up early is the caller's call: a barrier must keep waiting, since
|
|
45
|
+
# returning early would release the concurrency group with an operation
|
|
46
|
+
# still running.
|
|
47
|
+
fails=$(( fails + 1 ))
|
|
48
|
+
power=""
|
|
49
|
+
prov=""
|
|
50
|
+
echo "az aks show failed: $(tr '\n' ' ' <"$err_file")"
|
|
51
|
+
if [ "$max_fails" -gt 0 ] && [ "$fails" -ge "$max_fails" ]; then
|
|
52
|
+
echo "::error title=Cluster query failed::az aks show failed $fails times in a row for $cluster: $(tr '\n' ' ' <"$err_file")"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
if [ "${prov:-}" = "Succeeded" ] && { [ -z "$want_power" ] || [ "${power:-}" = "$want_power" ]; }; then
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
case "${prov:-}" in
|
|
60
|
+
Failed|Canceled)
|
|
61
|
+
# Terminal: nothing is in flight, so polling to the deadline would only
|
|
62
|
+
# delay the report and then blame the wrong thing.
|
|
63
|
+
echo "::error title=Cluster in a terminal state::$cluster provisioningState=$prov (power=${power:-unknown}); no operation is in flight."
|
|
64
|
+
exit 1
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
# Poll once more *at* the deadline rather than sleeping past it and failing
|
|
68
|
+
# on a stale read: an operation that settles during the final sleep still
|
|
69
|
+
# counts as settled within the advertised budget.
|
|
70
|
+
[ "$SECONDS" -lt "$deadline" ] || break
|
|
71
|
+
echo "waiting for $cluster to settle (${power:-unknown}/${prov:-unknown})"
|
|
72
|
+
sleep 20
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
echo "::error title=Cluster busy::$cluster is ${power:-unknown}/${prov:-unknown} after ${WAIT_AKS_TIMEOUT:-600}s; another operation has not finished."
|
|
76
|
+
exit 1
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
# Supersede a PR's older runs when new commits arrive. Non-PR events get a
|
|
11
|
+
# unique group: GitHub cancels a *pending* run when a newer one joins the
|
|
12
|
+
# group even with cancel-in-progress off, so sharing a group on main would
|
|
13
|
+
# let a burst of merges discard the middle commit's verification.
|
|
14
|
+
concurrency:
|
|
15
|
+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
|
16
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
# Docs are load-bearing for the suite - tests assert README, SECURITY.md,
|
|
20
|
+
# docs/keybindings.md and docs/windows.md still match the code and the
|
|
21
|
+
# release workflow - so a docs edit must still be tested. It does not need
|
|
22
|
+
# testing four times: those contracts are not Python-version or OS
|
|
23
|
+
# specific. Classify the change here so the redundant legs can skip the
|
|
24
|
+
# 20-30 minute pytest step while still reporting (they are required checks,
|
|
25
|
+
# and a required check that never runs blocks the merge forever).
|
|
26
|
+
changes:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
# Reading the PR's file list needs more than the workflow-level
|
|
29
|
+
# `contents: read`; without this the API call 404s on every PR.
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
pull-requests: read
|
|
33
|
+
outputs:
|
|
34
|
+
code: ${{ steps.classify.outputs.code }}
|
|
35
|
+
steps:
|
|
36
|
+
- name: Classify the change
|
|
37
|
+
id: classify
|
|
38
|
+
env:
|
|
39
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
PR: ${{ github.event.pull_request.number }}
|
|
41
|
+
REPO: ${{ github.repository }}
|
|
42
|
+
run: |
|
|
43
|
+
set -uo pipefail
|
|
44
|
+
# Fail closed: anything we cannot positively identify as docs-only
|
|
45
|
+
# gets the full matrix. That includes a failed API call - exiting
|
|
46
|
+
# non-zero here would *skip* the dependent test jobs rather than run
|
|
47
|
+
# them, and a skipped required check never reports.
|
|
48
|
+
if [ -z "${PR:-}" ]; then
|
|
49
|
+
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
50
|
+
echo "not a pull request; running everything"
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
if ! files=$(gh api --paginate "repos/$REPO/pulls/$PR/files" --jq '.[].filename'); then
|
|
54
|
+
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
55
|
+
echo "could not read the file list; running everything"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
echo "$files"
|
|
59
|
+
# Docs-only means every path is under docs/ or is a root .md file.
|
|
60
|
+
# grep must consume the whole list: `grep -q` stops at the first hit
|
|
61
|
+
# and SIGPIPEs the writer, which under pipefail fails the pipeline
|
|
62
|
+
# and would read as docs-only - skipping tests on a code change.
|
|
63
|
+
non_docs=$(printf '%s\n' "$files" | { grep -cvE '^(docs/.*|[^/]+\.md)$' || true; })
|
|
64
|
+
if [ "${non_docs:-1}" -gt 0 ]; then
|
|
65
|
+
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
66
|
+
else
|
|
67
|
+
echo "code=false" >> "$GITHUB_OUTPUT"
|
|
68
|
+
echo "docs-only: one full test pass instead of four"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
test:
|
|
72
|
+
needs: changes
|
|
73
|
+
runs-on: korvid-runners
|
|
74
|
+
strategy:
|
|
75
|
+
matrix:
|
|
76
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
79
|
+
with:
|
|
80
|
+
persist-credentials: false
|
|
81
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
82
|
+
with:
|
|
83
|
+
python-version: ${{ matrix.python-version }}
|
|
84
|
+
enable-cache: true
|
|
85
|
+
- run: uv sync --locked --dev --all-extras
|
|
86
|
+
- run: uv run ruff check src/ tests/
|
|
87
|
+
- run: uv run ruff format --check src/ tests/
|
|
88
|
+
- run: uv run mypy
|
|
89
|
+
- run: uv run tach check
|
|
90
|
+
- run: uvx deptry src/
|
|
91
|
+
# A docs-only change still gets a full suite run, on one version.
|
|
92
|
+
- if: needs.changes.outputs.code == 'true' || matrix.python-version == '3.12'
|
|
93
|
+
run: uv run pytest --cov --cov-fail-under=80
|
|
94
|
+
|
|
95
|
+
windows-test:
|
|
96
|
+
needs: changes
|
|
97
|
+
runs-on: windows-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
100
|
+
with:
|
|
101
|
+
persist-credentials: false
|
|
102
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
103
|
+
with:
|
|
104
|
+
python-version: "3.12"
|
|
105
|
+
enable-cache: true
|
|
106
|
+
- run: uv sync --locked --dev --all-extras
|
|
107
|
+
# Windows verifies OS-specific behaviour; docs contracts are not that.
|
|
108
|
+
- if: needs.changes.outputs.code == 'true'
|
|
109
|
+
run: uv run pytest -q
|
|
110
|
+
|
|
111
|
+
pre-commit:
|
|
112
|
+
runs-on: korvid-runners
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
115
|
+
with:
|
|
116
|
+
persist-credentials: false
|
|
117
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
118
|
+
with:
|
|
119
|
+
enable-cache: true
|
|
120
|
+
- run: uv sync --locked --dev --all-extras
|
|
121
|
+
- run: uv run pre-commit run --all-files --show-diff-on-failure
|
|
122
|
+
|
|
123
|
+
security:
|
|
124
|
+
runs-on: korvid-runners
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
127
|
+
with:
|
|
128
|
+
persist-credentials: false
|
|
129
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
130
|
+
with:
|
|
131
|
+
enable-cache: true
|
|
132
|
+
# Dependency vulnerability audit against the locked resolution.
|
|
133
|
+
# --all-extras, because `korvid[agent]` and `korvid[mcp]` are supported
|
|
134
|
+
# installs: auditing the base set alone leaves an httpx or starlette
|
|
135
|
+
# advisory invisible until release verification catches it.
|
|
136
|
+
- run: uv export --frozen --all-extras --no-emit-project --no-dev -o requirements.txt
|
|
137
|
+
- run: uvx pip-audit -r requirements.txt --disable-pip
|
|
138
|
+
# Static analysis of GitHub Actions workflows themselves
|
|
139
|
+
- run: uvx zizmor --min-severity medium .github/workflows/
|
|
140
|
+
|
|
141
|
+
dependency-review:
|
|
142
|
+
if: github.event_name == 'pull_request'
|
|
143
|
+
runs-on: korvid-runners
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
146
|
+
with:
|
|
147
|
+
persist-credentials: false
|
|
148
|
+
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
|
149
|
+
with:
|
|
150
|
+
fail-on-severity: high
|
|
151
|
+
|
|
152
|
+
ty-experimental:
|
|
153
|
+
runs-on: korvid-runners
|
|
154
|
+
continue-on-error: true
|
|
155
|
+
steps:
|
|
156
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
157
|
+
with:
|
|
158
|
+
persist-credentials: false
|
|
159
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
160
|
+
- run: uvx ty check src/ || true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CodeQL
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [main]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "17 4 * * 1"
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
# Same policy as CI: supersede a PR's older analyses, and keep every
|
|
14
|
+
# push/schedule analysis in its own group so a queued one is never displaced.
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
|
17
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
analyze:
|
|
21
|
+
runs-on: korvid-runners
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
security-events: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
|
30
|
+
with:
|
|
31
|
+
languages: python
|
|
32
|
+
queries: security-extended
|
|
33
|
+
- uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
name: K8s contract tests
|
|
2
|
+
# Live-cluster contract suite (issue #109). Runs the destructive
|
|
3
|
+
# write-path contract tests against the dedicated, stopped-at-rest
|
|
4
|
+
# AKS cluster `aks-korvid-contract-test`. Never runs for pull requests
|
|
5
|
+
# or forks: the Azure identity is scoped to the protected
|
|
6
|
+
# `aks-contract-test` environment, which only `main` may use.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: korvid-k8s-contract
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
env:
|
|
21
|
+
# The one cluster this workflow is allowed to touch. The guard steps
|
|
22
|
+
# fail closed when the (mutable) environment variables drift from
|
|
23
|
+
# these fixed, documented test-only names.
|
|
24
|
+
EXPECTED_RESOURCE_GROUP: rg-korvid-contract-test
|
|
25
|
+
EXPECTED_CLUSTER_NAME: aks-korvid-contract-test
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
contract:
|
|
29
|
+
# GitHub-hosted ephemeral runner — deliberately NOT the shared
|
|
30
|
+
# self-hosted `korvid-runners` pool, so cluster credentials never
|
|
31
|
+
# touch persistent shared infrastructure.
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: aks-contract-test
|
|
34
|
+
timeout-minutes: 45
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
id-token: write
|
|
38
|
+
env:
|
|
39
|
+
AKS_RESOURCE_GROUP: ${{ vars.AKS_RESOURCE_GROUP }}
|
|
40
|
+
AKS_CLUSTER_NAME: ${{ vars.AKS_CLUSTER_NAME }}
|
|
41
|
+
# Named only in the guard's repair hint. Fixed rather than a variable so
|
|
42
|
+
# a drifting setting cannot send someone to scale the system pool.
|
|
43
|
+
WORKLOAD_NODEPOOL: workload
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
46
|
+
with:
|
|
47
|
+
persist-credentials: false
|
|
48
|
+
|
|
49
|
+
- name: Guard — refuse any target other than the documented test cluster
|
|
50
|
+
run: |
|
|
51
|
+
if [ "$AKS_RESOURCE_GROUP" != "$EXPECTED_RESOURCE_GROUP" ] || \
|
|
52
|
+
[ "$AKS_CLUSTER_NAME" != "$EXPECTED_CLUSTER_NAME" ]; then
|
|
53
|
+
echo "::error title=Cluster identity mismatch::environment variables point at $AKS_RESOURCE_GROUP/$AKS_CLUSTER_NAME, expected $EXPECTED_RESOURCE_GROUP/$EXPECTED_CLUSTER_NAME. Refusing to run destructive tests."
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
- uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
|
58
|
+
with:
|
|
59
|
+
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
|
60
|
+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
|
61
|
+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
|
62
|
+
|
|
63
|
+
- name: Guard — verify test-only tags before touching the cluster
|
|
64
|
+
run: |
|
|
65
|
+
purpose=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
|
|
66
|
+
--query 'tags.purpose' -o tsv)
|
|
67
|
+
production=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
|
|
68
|
+
--query 'tags."production-use"' -o tsv)
|
|
69
|
+
if [ "$purpose" != "korvid-contract-testing" ] || [ "$production" != "prohibited" ]; then
|
|
70
|
+
echo "::error title=Not a test-only cluster::$AKS_CLUSTER_NAME is missing the purpose=korvid-contract-testing / production-use=prohibited tags. Refusing to run destructive tests."
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
- name: Start AKS cluster
|
|
75
|
+
run: |
|
|
76
|
+
# A preceding run's stop can still be finishing even though
|
|
77
|
+
# powerState already reads Stopped; Azure rejects a start until it
|
|
78
|
+
# settles. Not a barrier - failing here fails this job and cleanup
|
|
79
|
+
# still runs - so give up early on a broken query rather than
|
|
80
|
+
# burning ten minutes on it.
|
|
81
|
+
WAIT_AKS_MAX_QUERY_FAILURES=3 \
|
|
82
|
+
.github/scripts/wait-aks-settled.sh "$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME"
|
|
83
|
+
state=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv)
|
|
84
|
+
if [ "$state" != "Running" ]; then
|
|
85
|
+
az aks start -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
- uses: azure/use-kubelogin@0ce7c36141aa27d4934872cf00b0120804c98a29 # v1.3
|
|
89
|
+
with:
|
|
90
|
+
kubelogin-version: v0.2.15
|
|
91
|
+
env:
|
|
92
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
93
|
+
|
|
94
|
+
- name: Fetch user credentials (no --admin, isolated kubeconfig)
|
|
95
|
+
run: |
|
|
96
|
+
export KUBECONFIG="$RUNNER_TEMP/contract-kubeconfig"
|
|
97
|
+
az aks get-credentials -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" \
|
|
98
|
+
--overwrite-existing -f "$KUBECONFIG"
|
|
99
|
+
kubelogin convert-kubeconfig -l azurecli --kubeconfig "$KUBECONFIG"
|
|
100
|
+
chmod 600 "$KUBECONFIG"
|
|
101
|
+
echo "KUBECONFIG=$KUBECONFIG" >> "$GITHUB_ENV"
|
|
102
|
+
|
|
103
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
104
|
+
with:
|
|
105
|
+
python-version: "3.13"
|
|
106
|
+
enable-cache: true
|
|
107
|
+
|
|
108
|
+
- run: uv sync --locked --dev --all-extras
|
|
109
|
+
|
|
110
|
+
- name: Install helm
|
|
111
|
+
run: |
|
|
112
|
+
HELM_VERSION=v3.19.0
|
|
113
|
+
curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" \
|
|
114
|
+
| tar -xz -C "$RUNNER_TEMP" linux-amd64/helm
|
|
115
|
+
sudo install "$RUNNER_TEMP/linux-amd64/helm" /usr/local/bin/helm
|
|
116
|
+
|
|
117
|
+
- name: Janitor — remove stale fixtures from interrupted runs
|
|
118
|
+
run: uv run python -m tests.contract.janitor
|
|
119
|
+
|
|
120
|
+
- name: Guard — a schedulable disposable node must exist
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
# The suite needs one node that satisfies both of its requirements at
|
|
124
|
+
# once: the node-operation contracts only touch
|
|
125
|
+
# korvid.dev/disposable=true and fail closed rather than fall back to
|
|
126
|
+
# a system node, while every test pod carries a
|
|
127
|
+
# korvid.dev/pool=workload nodeSelector. Those labels riding the same
|
|
128
|
+
# node is a convention of the pool setup, not something enforced
|
|
129
|
+
# here, so both are required together — a node with only one of them
|
|
130
|
+
# would let this guard pass and leave pods Pending.
|
|
131
|
+
#
|
|
132
|
+
# With no such node the suite produces six failures across four files
|
|
133
|
+
# — "no disposable workload node found" and pod-scheduling timeouts —
|
|
134
|
+
# none of which names the cause. It stayed red for eight commits.
|
|
135
|
+
#
|
|
136
|
+
# Asked of the cluster rather than of Azure: a stopped or
|
|
137
|
+
# still-starting cluster reports agentPools[].count as 0 even when
|
|
138
|
+
# the pool is populated, so the control-plane number is not evidence.
|
|
139
|
+
#
|
|
140
|
+
# Checked, not repaired: the workflow identity holds a deliberately
|
|
141
|
+
# minimal role with agentPools/read and no write, and widening it so
|
|
142
|
+
# this job could scale the pool would also let it reshape the system
|
|
143
|
+
# pool that every other guard here exists to protect.
|
|
144
|
+
SELECTOR=korvid.dev/disposable=true,korvid.dev/pool=workload
|
|
145
|
+
|
|
146
|
+
# Prove the node list is reachable first. Otherwise an expired token,
|
|
147
|
+
# an RBAC denial, or an unreachable API server spends the whole
|
|
148
|
+
# deadline and then blames an empty pool, sending the reader after a
|
|
149
|
+
# scale command that cannot help.
|
|
150
|
+
if ! nodes=$(kubectl get nodes -o name 2>&1); then
|
|
151
|
+
echo "::error title=Cannot query nodes::Listing nodes on $AKS_CLUSTER_NAME failed, so the disposable-node check could not run: $nodes"
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# `kubectl wait` requires *every* node matching the selector to
|
|
156
|
+
# satisfy the condition, but this guard needs only one somewhere to
|
|
157
|
+
# schedule. A second pool node, or one cycling through an image
|
|
158
|
+
# upgrade, would otherwise burn the whole deadline and report "no
|
|
159
|
+
# schedulable node" while a perfectly good one was sitting there.
|
|
160
|
+
#
|
|
161
|
+
# Ready alone is not enough: a crashed node-ops test leaves the node
|
|
162
|
+
# cordoned, and a cordoned node would pass a Ready-only check and
|
|
163
|
+
# leave every test pod Pending. This runs after the janitor, which
|
|
164
|
+
# uncordons disposable nodes, so anything still cordoned here is
|
|
165
|
+
# something the janitor could not fix and a human has to look at.
|
|
166
|
+
deadline=$(( SECONDS + 300 ))
|
|
167
|
+
while :; do
|
|
168
|
+
if kubectl get nodes -l "$SELECTOR" \
|
|
169
|
+
-o jsonpath='{range .items[*]}{.metadata.name}{"=ready:"}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{" unschedulable:"}{.spec.unschedulable}{"\n"}{end}' \
|
|
170
|
+
2>/dev/null | grep -q '=ready:True unschedulable:$'; then
|
|
171
|
+
kubectl get nodes -l "$SELECTOR"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
[ "$SECONDS" -lt "$deadline" ] || break
|
|
175
|
+
sleep 5
|
|
176
|
+
done
|
|
177
|
+
echo "::error title=No schedulable disposable node::$AKS_CLUSTER_NAME has no Ready, uncordoned node matching $SELECTOR after 5 minutes, so the node-operation and resize contracts have nowhere to schedule. If a previous run left the node cordoned, uncordon it. If the $WORKLOAD_NODEPOOL pool is empty, restore it by starting the cluster first — this run's cleanup job stops it, and Azure rejects every operation except start on a stopped cluster: az aks start -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME && az aks nodepool scale -g $AKS_RESOURCE_GROUP --cluster-name $AKS_CLUSTER_NAME -n $WORKLOAD_NODEPOOL --node-count 1 && az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
|
|
178
|
+
kubectl get nodes -o wide --show-labels || true
|
|
179
|
+
exit 1
|
|
180
|
+
|
|
181
|
+
- name: Run contract suite
|
|
182
|
+
timeout-minutes: 30
|
|
183
|
+
env:
|
|
184
|
+
KORVID_CONTRACT_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
185
|
+
run: uv run pytest -p no:randomly -m contract tests/contract/ -v
|
|
186
|
+
|
|
187
|
+
- name: Collect diagnostics
|
|
188
|
+
if: failure() || cancelled()
|
|
189
|
+
run: |
|
|
190
|
+
mkdir -p diag
|
|
191
|
+
kubectl get events -A --sort-by=.lastTimestamp > diag/events.txt || true
|
|
192
|
+
kubectl get pods -A -o wide > diag/pods.txt || true
|
|
193
|
+
kubectl describe pods -A > diag/pods-describe.txt || true
|
|
194
|
+
kubectl get nodes -o wide > diag/nodes.txt || true
|
|
195
|
+
kubectl get ns -l app.kubernetes.io/managed-by=korvid-contract -o yaml > diag/namespaces.yaml || true
|
|
196
|
+
|
|
197
|
+
- name: Upload diagnostics
|
|
198
|
+
if: failure() || cancelled()
|
|
199
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
200
|
+
with:
|
|
201
|
+
name: contract-diagnostics
|
|
202
|
+
path: diag/
|
|
203
|
+
retention-days: 14
|
|
204
|
+
|
|
205
|
+
- name: Delete kubeconfig
|
|
206
|
+
if: always()
|
|
207
|
+
run: |
|
|
208
|
+
if [ -n "${KUBECONFIG:-}" ] && [ -f "$KUBECONFIG" ]; then
|
|
209
|
+
rm -f "$KUBECONFIG"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Separate job so the stop runs even when the contract job hits its
|
|
213
|
+
# timeout-minutes (GitHub kills the job before later always() steps,
|
|
214
|
+
# which would leave the stopped-at-rest cluster running and billing).
|
|
215
|
+
stop-cluster:
|
|
216
|
+
needs: contract
|
|
217
|
+
if: always()
|
|
218
|
+
runs-on: ubuntu-latest
|
|
219
|
+
environment: aks-contract-test
|
|
220
|
+
timeout-minutes: 20
|
|
221
|
+
permissions:
|
|
222
|
+
contents: read
|
|
223
|
+
id-token: write
|
|
224
|
+
env:
|
|
225
|
+
AKS_RESOURCE_GROUP: ${{ vars.AKS_RESOURCE_GROUP }}
|
|
226
|
+
AKS_CLUSTER_NAME: ${{ vars.AKS_CLUSTER_NAME }}
|
|
227
|
+
steps:
|
|
228
|
+
# The settle helper lives in the repo, so this job needs the tree too.
|
|
229
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
230
|
+
with:
|
|
231
|
+
persist-credentials: false
|
|
232
|
+
|
|
233
|
+
- name: Guard — refuse any target other than the documented test cluster
|
|
234
|
+
run: |
|
|
235
|
+
if [ "$AKS_RESOURCE_GROUP" != "$EXPECTED_RESOURCE_GROUP" ] || \
|
|
236
|
+
[ "$AKS_CLUSTER_NAME" != "$EXPECTED_CLUSTER_NAME" ]; then
|
|
237
|
+
echo "::error title=Cluster identity mismatch::environment variables point at $AKS_RESOURCE_GROUP/$AKS_CLUSTER_NAME, expected $EXPECTED_RESOURCE_GROUP/$EXPECTED_CLUSTER_NAME. Refusing to stop an unexpected cluster."
|
|
238
|
+
exit 1
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
- uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
|
|
242
|
+
with:
|
|
243
|
+
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
|
244
|
+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
|
245
|
+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
|
246
|
+
|
|
247
|
+
- name: Stop AKS cluster
|
|
248
|
+
run: |
|
|
249
|
+
# A test may have left an operation in flight (a nodepool scale, a
|
|
250
|
+
# failed run mid-write) and Azure would reject the stop outright.
|
|
251
|
+
# Advisory only: it must never prevent the stop, because a step that
|
|
252
|
+
# dies here leaves the cluster running with no recovery hint - so a
|
|
253
|
+
# failed or timed-out wait falls through and lets the stop try
|
|
254
|
+
# anyway, and its own error is the report.
|
|
255
|
+
WAIT_AKS_TIMEOUT=300 WAIT_AKS_MAX_QUERY_FAILURES=3 \
|
|
256
|
+
.github/scripts/wait-aks-settled.sh "$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME" \
|
|
257
|
+
|| echo "cluster did not settle before the stop; attempting it anyway"
|
|
258
|
+
# Azure rejects a stop on a cluster that is not running, which is the
|
|
259
|
+
# normal state when the contract job failed before starting it.
|
|
260
|
+
state=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv 2>/dev/null || echo unknown)
|
|
261
|
+
if [ "$state" = "Stopped" ]; then
|
|
262
|
+
echo "$AKS_CLUSTER_NAME is already stopped"
|
|
263
|
+
elif ! az aks stop -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --no-wait; then
|
|
264
|
+
# The state read above can be `unknown` when the query itself
|
|
265
|
+
# failed, so a rejection here may just mean it was already stopped.
|
|
266
|
+
# Check before crying wolf: a red cleanup job for a cluster that is
|
|
267
|
+
# costing nothing trains people to ignore this error.
|
|
268
|
+
recheck=$(az aks show -g "$AKS_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query powerState.code -o tsv 2>/dev/null || echo unknown)
|
|
269
|
+
if [ "$recheck" = "Stopped" ]; then
|
|
270
|
+
echo "$AKS_CLUSTER_NAME was already stopped"
|
|
271
|
+
else
|
|
272
|
+
echo "::error title=AKS stop rejected::aks-korvid-contract-test would not accept a stop and reads $recheck; idle compute may be accruing. Stop it manually once its current operation finishes: az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
|
|
273
|
+
exit 1
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
# powerState flips to Stopped as soon as Azure accepts the request,
|
|
277
|
+
# while provisioningState stays Stopping until the operation really
|
|
278
|
+
# finishes. Waiting on powerState alone let this job exit seconds
|
|
279
|
+
# after asking, so the next merge's run hit "there's an in-progress
|
|
280
|
+
# stop managed cluster operation" on its own start. This one is a
|
|
281
|
+
# barrier, so it retries query failures through the whole budget.
|
|
282
|
+
if ! WAIT_AKS_TIMEOUT=600 .github/scripts/wait-aks-settled.sh \
|
|
283
|
+
"$AKS_RESOURCE_GROUP" "$AKS_CLUSTER_NAME" Stopped; then
|
|
284
|
+
echo "::error title=AKS stop failed::aks-korvid-contract-test did not settle at Stopped; idle compute may be accruing. Stop it manually: az aks stop -g $AKS_RESOURCE_GROUP -n $AKS_CLUSTER_NAME"
|
|
285
|
+
exit 1
|
|
286
|
+
fi
|