This project accepts contributions in the form of bug reports, feature requests, and pull requests (see below). If you are looking to contribute code, please follow the guidelines outlined in this document.
KSail is licensed under the PolyForm Shield License 1.0.0. By submitting a pull request, you agree that your contributions will be licensed under the same terms. This means:
- Your contributed code is licensed under PolyForm Shield 1.0.0 — anyone may use it except to compete with KSail
- You must have the right to submit the code under this license
- If your contribution includes third-party code, it must be compatible with PolyForm Shield 1.0.0
New to KSail? Welcome! Here's how to get started:
- Find an issue — Look for issues labeled
good first issuefor beginner-friendly tasks, orhelp wantedfor tasks where the maintainers are looking for help. - Fork & clone — Fork the repository and clone it locally.
- Set up your environment — Follow the Prerequisites section below.
- Make your changes — Create a branch, implement your fix or feature, and write tests.
- Submit a PR — Open a pull request against
mainwith a clear description of what changed and why.
If you have questions, don't hesitate to ask in GitHub Discussions.
A unit-tested change in one package, or a fix to a guide outside the documentation site (such as this file or a README.md), needs only Go — plus, on macOS, GNU coreutils, because the CI-harness tests in the whole unit suite need GNU timeout (see Prerequisites). You do not need Docker, Node.js or a cloud account for it:
# working-directory: ./
go build ./... # everything compiles
go test ./pkg/<package>/... # the package you changed
go test ./... # the whole unit suite, before you open the PRA change under docs/ is different: it is part of the documentation site, so it also needs Node.js and a site build before you open the pull request (see Building the documentation).
In the pull request, say which of these you ran. If your change touches cluster creation, a cloud provider, the release pipeline or a deployment and you could not run that part yourself, say so plainly. On a pull request, CI starts the Docker system tests by itself only when the pull request comes from a branch in this repository and changes a file they depend on; a change that touches none of those files (most documentation changes) and a pull request from a fork skip them. They can also be started by hand for any branch in this repository (see System Tests). The maintainers run the cloud and release checks. A passing unit suite is not proof that a cluster, a release or a deployment works.
To get started with contributing to KSail, you'll need to set up your development environment, and understand the codebase, the CI setup and its requirements.
To understand the codebase, it is recommended to read the AGENTS.md file, which provides an overview of the project structure and key components. You can also use GitHub Copilot to assist you in navigating the codebase and understanding its functionality.
For a deeper dive into KSail's design and internals, refer to:
- Architecture Guide — Design principles, component architecture, provider/provisioner model, and state persistence
- Development Guide — Development environment setup, coding standards, testing patterns, and CI/CD workflows
For detailed package and API documentation, refer to the Go documentation at pkg.go.dev/github.com/devantler-tech/ksail/v7. This provides comprehensive documentation for all exported packages, types, functions, and methods.
Runtime Requirements:
- Docker — The only external dependency for local clusters (the Docker provider, which every distribution except the managed cloud ones can use)
- Cloud providers need their own account and credentials, and only when you use them: Hetzner (
HCLOUD_TOKEN), Sidero Omni (OMNI_SERVICE_ACCOUNT_KEY), and AWS, Google Cloud or Azure for the managed distributions. EKS also needs theeksctlbinary on yourPATH; it is the one tool KSail does not embed.
Development Requirements:
Before you begin developing, ensure you have the following installed:
- Go — the version in the
goline ofgo.modor newer (CI installs exactly that version) - mockery (v3.5+)
- golangci-lint
- mega-linter
- Node.js (v24+) — Required for the documentation site, the web UI (and therefore
make build) and the VSCode extension; CI uses Node.js 24. A plaingo builddoes not need it. - GNU coreutils — The CI harness uses GNU
timeoutto test hanging processes. On macOS, installcoreutils; the harness also recognizes itsgtimeoutcommand.
Follow standard Go conventions in this repository:
- Formatting: Format code with
gofmtand keep imports organized withgoimports(or rungolangci-lint run --fix). Keep lines under 100 characters (enforced bygolines). - Naming: Exported identifiers use
PascalCase; unexported identifiers usecamelCase. Package names are short, lowercase, and singular (e.g.,provider,installer). Interfaces often end in-er(e.g.,Provider). - Error handling: Return errors explicitly — no
panicin library code. Wrap errors with context usingfmt.Errorf("context: %w", err). All errors must be checked (golangci-lintenforces this). - Testing: Prefer table-driven tests with
t.Parallel(). Keep tests focused on public behavior. Rungo test ./...before opening a PR. - Path safety: Canonicalize all user-supplied file and directory paths with
fsutil.EvalCanonicalPathbefore use (resolves symlinks, prevents path-escape attacks). Preferfsutil.ReadFileSafefor constrained reads instead of reimplementing path-containment checks. For output paths that may not exist yet, create the parent directory withos.MkdirAllfirst, then canonicalize. - Documentation: Document all exported types, functions, and constants with complete sentences starting with the name being documented (e.g.,
// Provider defines the interface for infrastructure providers.).
KSail uses mega-linter with the go flavor, and uses a strict configuration to ensure code quality and consistency. You can run the linter with the following command:
# working-directory: ./
mega-linter-runner -f goThe same configuration is used in CI, so you can expect the same linting behavior in your local environment as in the CI pipeline.
MegaLinter also checks Markdown files. Markdown lint rules are configured in .markdownlint.json (some rules are relaxed to accommodate Astro/Starlight front matter and documentation formatting).
# working-directory: ./
# Build the ksail binary with the web UI embedded (needs Node.js)
make build
# Or: build the CLI only (Go only; `ksail open web` then serves a placeholder page)
go build -o ksail
# Or: compile all packages (no binary output)
go build ./...
# For optimized builds (strips debug symbols):
go build -ldflags="-s -w" -o ksail-optimizedThe web UI is not checked in. make build builds it from web/ui/ and stages it into pkg/webui/dist/ before compiling, so the binary serves the real UI. A plain go build skips that step: every CLI command works, but the embedded web UI is a placeholder. Use the plain build for CLI work and make build when you touch or need the web UI.
Note: Release builds use
-ldflags="-s -w -X .../buildmeta.Version=... -X .../buildmeta.Commit=... -X .../buildmeta.Date=...". The-s -wflags strip debug symbols (reducing binary size significantly) and the-Xflags inject version metadata via GoReleaser. Development builds retain debug symbols for a better debugging experience.
The CLI and desktop app share the root Go module. Desktop source and tests require
-tags desktop, leaving ordinary CLI builds and tests independent of the system webview.
Run go mod tidy at the repository root for both entry points.
make desktop # builds the web UI and native binary
make desktop-app # macOS app bundle
go test -tags desktop ./desktop # native tests; requires the platform webview toolchain
golangci-lint run --build-tags desktop ./desktop/... # native lintLinux requires GTK4 and WebKitGTK 6.0 development packages; macOS requires Xcode command line tools. Desktop CI runs native tests on Linux, macOS and Windows and checks the macOS app/cask packaging.
# working-directory: ./
mockeryTip:
make generateruns mockery together with every other generator in the repository — see Auto-generated artifacts.
# working-directory: ./
go test ./...The project documentation is built using Astro with the Starlight theme and is located in the docs/ directory.
Several files are generated from source and should not be edited manually. make generate is THE regeneration command — it runs every generator in dependency order:
# working-directory: ./
make generateThe individual generators, for reference (only needed when you want to run a single one):
- Configuration schema:
schemas/ksail-config.schema.json—go generate ./schemas/... - CRD & deepcopy:
charts/ksail-operator/crds/andpkg/apis/cluster/v1alpha1/zz_generated.deepcopy.go—go generate ./pkg/apis/... - CLI flags reference (
docs/src/content/docs/cli-flags/, from the Cobra command tree) and configuration reference (docs/src/content/docs/configuration/declarative-configuration.mdx, from v1alpha1 types) —go generate ./docs/... - Chat assistant docs:
pkg/svc/chat/docs_generated.go—go generate ./pkg/svc/chat/...(reads the generated reference docs, so rungo generate ./docs/...first) - Mocks:
mocks.gofiles —mockery - Web UI config types:
web/ui/src/generated/ksail-config.ts—npm --prefix web/ui run gen:types(reads the JSON schema, so rungo generate ./schemas/...first)
CI's generate job regenerates all of these (except mocks, which only change when .mockery.yml or mocked interfaces change) on pull requests and auto-commits any drift (fork PRs fail with instructions instead), so stale generated files never land on main.
# working-directory: ./docs
# Install dependencies (first time only or when package-lock.json changes)
npm ci
# Build the site
npm run build
# Serve the site locally with live reload (optional)
npm run dev
# Visit http://localhost:4321 to view the siteThe built site will be available in docs/dist/. Note that docs/dist/ and docs/node_modules/ are excluded from git via .gitignore.
The VSCode extension is located in the vsce/ directory and provides cluster management capabilities directly in VSCode.
# working-directory: ./vsce
# Install dependencies (first time only or when package-lock.json changes)
npm ci
# Compile TypeScript to JavaScript
npm run compile
# Package as VSIX for distribution
npx @vscode/vsce package --no-dependencies- Open the
vscefolder in VSCode - Press
F5to launch Extension Development Host - Test commands from the Command Palette (
Cmd+Shift+P/Ctrl+Shift+P)
The extension source is organized as follows:
vsce/
├── src/
│ ├── extension.ts # Entry point, command registration
│ ├── commands/
│ │ ├── index.ts # Command handlers (command registry)
│ │ └── prompts.ts # Interactive wizard implementations
│ ├── ksail/
│ │ ├── clusters.ts # KSail CLI wrapper functions
│ │ ├── clusterList.ts # Cluster list parsing
│ │ ├── contexts.ts # Shared helpers for kubeconfig context names
│ │ ├── enums.ts # Distribution, provider and option values
│ │ ├── binary.ts # KSail binary discovery and execution
│ │ ├── kubectl.ts # kubectl wrapper for cluster status queries
│ │ └── index.ts # Module exports
│ ├── kubernetes/
│ │ ├── cloudProvider.ts # Cloud Explorer tree provider (KSail clusters in Clouds view)
│ │ ├── clusterExplorerContributor.ts # Annotates KSail contexts in Cluster Explorer
│ │ ├── clusterProvider.ts # Create Cluster wizard (HTML-based)
│ │ └── index.ts # Module exports
│ ├── mcp/
│ │ ├── serverProvider.ts # MCP server definition provider
│ │ └── index.ts # Module exports
│ └── test/ # Extension tests
├── dist/ # Compiled output
└── package.json # Extension manifest
See vsce/README.md for end-user feature documentation, or ksail.devantler.tech/vscode-extension for the full docs site page.
The repository is organized around the top-level CLI entry point (main.go) and the public packages in pkg/. See AGENTS.md for the full annotated structure tree.
- main.go - CLI entry point
- pkg/cli/cmd/ - CLI command implementations
- pkg/ - Public packages (importable by external projects)
- internal/ - Private packages (build metadata, operator reconcilers, test utilities, and the CI, release and dependency contract checks)
- charts/ - Helm charts (
charts/ksail-operator/— operator + embedded web UI) - web/ - Web UI source (
web/ui/— Vite/React SPA, embedded viapkg/webui) - desktop/ - Native desktop app (shared Go module; opt in with -tags desktop)
- copilot-plugin/ - ksail agent skill, published as a plugin via devantler-tech/agent-plugins
- docs/ - Astro documentation site
- vsce/ - VSCode extension
- apis/ - API types, schemas, and enums (distribution/provider values)
- client/ - Tool clients (kubectl, helm, flux, argocd, docker, k9s, kubeconform, kubescape, kustomize, oci, netretry, sops, klogutil, hubble, celrules, and the managed-cloud clients eks, gke and aks; eksctl shells out to an external binary); distribution tools like kind, k3d, and vcluster are used directly via their SDKs in provisioners, not wrapped in
pkg/client/ - client/reconciler/ - Common base for GitOps reconciliation clients (Flux and ArgoCD)
- operator/ - Kubernetes operator manager and REST API server (reconcilers live in
internal/controller/) - webui/ - Embedded web UI assets (built from
web/ui/, served byksail open weband the operator) - svc/detector/ - Detects installed Kubernetes components (Helm releases and Kubernetes API); used by the update command to build accurate baseline cluster state
- svc/diff/ - Computes configuration differences between ClusterSpec values; classifies update impact (in-place, reboot-required, recreate-required)
- svc/image/ - Container image export/import services for Vanilla and K3s distributions
- svc/installer/ - Component installers (CNI, CSI, metrics-server, etc.)
- svc/provider/ - Infrastructure providers (e.g.,
docker.Providerfor running nodes as containers) - svc/provisioner/ - Distribution provisioners (Vanilla, K3s, Talos, VCluster, KWOK, EKS, GKE, AKS)
- svc/registryresolver/ - OCI registry detection, resolution, and artifact push utilities
- svc/state/ - Cluster state persistence for distributions that cannot introspect their running configuration (Kind, K3d)
- di/ - Dependency injection for wiring components
KSail separates infrastructure management from distribution configuration:
- Providers manage the infrastructure lifecycle (start/stop containers)
- Provisioners configure and manage Kubernetes distributions
| Distribution | Provisioner | Tool | Provider | Description |
|---|---|---|---|---|
Vanilla |
KindClusterProvisioner | Kind | Docker, Hetzner, Kubernetes | Standard upstream Kubernetes |
K3s |
K3dClusterProvisioner | K3d | Docker, Hetzner, Kubernetes | Lightweight K3s |
Talos |
TalosProvisioner | Talos | Docker, Hetzner, Omni, Kubernetes | Immutable Talos Linux |
VCluster |
VClusterProvisioner | Vind | Docker, Kubernetes | Virtual clusters via vCluster (Vind) |
KWOK |
KWOKProvisioner | kwokctl | Docker, Kubernetes | Simulated Kubernetes cluster (no real workloads) |
EKS |
EKSProvisioner | eksctl | AWS | Managed Kubernetes on Amazon Web Services |
GKE |
gke.Provisioner |
Google Cloud SDK | GCP | Managed Kubernetes on Google Cloud |
AKS |
aks.Provisioner |
Azure SDK | Azure | Managed Kubernetes on Microsoft Azure |
The Kubernetes provider runs cluster nodes as pods inside an existing cluster. The supported combinations are defined in pkg/apis/cluster/v1alpha1/distributionmeta.go, which is the source of truth for this table.
KSail's code is publicly available and reusable. All core functionality is implemented in the pkg/ directory so external projects can import and use any package under pkg/. The internal/ directory holds packages not useful to external consumers, such as internal/buildmeta (build-time version metadata injected via ldflags), internal/controller (the controller-runtime reconcilers behind the KSail operator), internal/testutil (shared test utilities) and the checks CI runs on the repository itself (for example internal/ciharness and internal/releasecontract).
For detailed package and API documentation, refer to pkg.go.dev/github.com/devantler-tech/ksail/v7.
# working-directory: ./
go test ./...System tests exercise full cluster lifecycle scenarios on real clusters. They are split by where the cluster runs:
| Workflow | Covers | Runs |
|---|---|---|
ci.yaml (system-test-docker) |
Vanilla, K3s, Talos, VCluster and KWOK on the Docker provider | On pull requests (see below) and on manual dispatch |
system-test-hetzner.yaml |
Talos in full; K3s and Vanilla as a smoke check, on Hetzner | Weekly (Sunday 03:00 UTC) and on manual dispatch |
system-test-omni.yaml |
Talos on Sidero Omni | Manual dispatch only (the schedule is paused) |
system-test-eks.yaml |
EKS on AWS | Manual dispatch only |
system-test-k3k-calico.yaml |
The nested Kubernetes provider with Calico | Weekly (Monday 02:30 UTC), on manual dispatch, and on pull requests that change the workflow itself |
All of them except the EKS workflow use the composite action at .github/actions/ksail-system-test/action.yaml. In the Hetzner workflow only the Talos legs do: its K3s and Vanilla legs are smoke checks that create the cluster, wait for the API server and delete it, so they do not exercise workloads, update, stop or start. The EKS workflow has its own init, create, update and cleanup steps, so a change to the composite action does not change what it covers.
When the Docker system tests run:
The system-test-docker job in .github/workflows/ci.yaml runs on a pull request when both of these hold:
- the pull request changes a file the system tests depend on (the
system-testpath list in thechangesjob ofci.yaml). Most documentation changes are not on that list and skip the tests, but everything under.github/actions/is, including the guides next to the actions; - the pull request comes from a branch in this repository. Pull requests from forks do not run them; they can be started by manual dispatch once the change is on a branch in this repository.
When it runs, it is part of CI - Required Checks, so a failure blocks the pull request. It does not run in the merge queue.
The matrix runs each of the five distributions against several configuration variants (default, a full component stack, an alternative stack, and Flux and ArgoCD with a registry). Projects are always created with ksail project init first; the path without a ksail.yaml is covered by unit tests instead. The exact matrix is in the job definition.
The cloud workflows (Hetzner, Omni, EKS) are separate because they cost money, need credentials and depend on infrastructure outside the repository. They never run on a pull request and are not required checks.
Manual trigger:
You can manually trigger the Docker system tests from any branch in this repository using workflow_dispatch:
gh workflow run ci.yaml --ref your-branch --field run_system_tests=trueThis is useful for validating infrastructure-sensitive changes that the path list does not pick up.
Checks that need no cluster:
workload gen subcommands and the open mcp --help smoke check are covered by unit tests, which run on every pull request with Go changes (open chat --help has no check of its own; unit tests cover the chat command's flags and helpers). They need neither Docker nor a cluster. The exceptions are workload gen clusterrole and workload gen role: they ask the API server which resources exist, so their coverage is the online phase of the system tests, and a change to either needs a system-test run.
Note: cipher encrypt/decrypt E2E testing is not currently possible because the encrypt command uses hardcoded empty key groups (no .sops.yaml config loading). Cipher commands are covered by unit tests in pkg/cli/cmd/workload/cipher/.
What the system test covers:
Each matrix job runs a full cluster lifecycle: init → create → workload deployment → update (regression detection) → stop → start, along with workload read operations (get, describe, logs) and scaling. The cluster is always deleted at the end. By default the action does it; the Docker job in CI turns that off so it can upload diagnostics first, and then deletes the cluster in a dedicated cleanup step that runs even after a failure. See .github/actions/ksail-system-test/action.yaml for the complete test sequence.
If system tests fail:
The pull request is blocked until the failure is resolved. The CI includes a comprehensive debug action (.github/actions/debug-kubernetes-failure/) that collects Kubernetes diagnostics (node status, pod status, events, component logs) to aid investigation.
To test the Hetzner provider locally, you need:
HCLOUD_TOKEN– Hetzner Cloud API token with read/write permissions- Talos ISO – A Talos Linux ISO must be available in your Hetzner Cloud project. KSail tracks Hetzner's latest compatible public x86 ISO from the official changelog; verify that default under Images → ISOs in the Hetzner Cloud Console or configure another available ISO explicitly.
Note: Some unit tests and CLI code paths enable Hetzner functionality when HCLOUD_TOKEN is set. If you’re not intentionally testing Hetzner, unset HCLOUD_TOKEN (or set it to an empty value) before running go test ./... to keep tests hermetic.
Note: Hetzner tests incur cloud costs. Use ksail cluster delete to clean up resources.
Note: The Hetzner workflow (.github/workflows/system-test-hetzner.yaml) ends with a safety-net cleanup job that always runs, also after a failure. It uses the action at .github/actions/cleanup-hetzner/action.yaml and is not intended for local execution.
Cleanup boundary: the job deletes only the resources of the clusters that run created. Each call selects one cluster by its ksail.cluster.name label, which includes the workflow run id. It removes that cluster's servers, placement groups, firewalls, networks and floating IPs (a floating IP only when it also carries ksail.owned=true). It does not touch other clusters in the project, including ones you created by hand. Remove anything else through the Hetzner Cloud Console or the hcloud CLI.
To test the Omni provider locally, you need:
OMNI_SERVICE_ACCOUNT_KEY– A Sidero Omni service account key with cluster management permissions. The environment variable name is configurable viaspec.provider.omni.serviceAccountKeyEnvVarinksail.yaml.- Omni endpoint – The URL of your Sidero Omni instance. Set it in the
OMNI_ENDPOINTenvironment variable (the name is configurable viaspec.provider.omni.endpointEnvVar) or asspec.provider.omni.endpointinksail.yaml. When both are set, the environment variable wins.
Note: Omni requires a Sidero Omni account and does not run locally. Omni manages the Talos machine lifecycle; StartNodes and StopNodes are no-ops in the Omni provider.
CI integration: Omni system tests have their own workflow, .github/workflows/system-test-omni.yaml. It runs on manual dispatch only: its weekly schedule is paused until a machine is registered in the Omni instance. It runs the same system-test action against a live Omni endpoint and is not a required check, so it does not block a pull request. It needs:
secrets.OMNI_SERVICE_ACCOUNT_KEY– Repository secret containing the Omni service account key.vars.OMNI_ENDPOINT– Repository variable containing the Omni instance URL.
The workflow also sets KSAIL_SPEC_CLUSTER_OMNI_MACHINECLASS to ksail via env; this specifies the Omni machine class used for test nodes.
Note: The Omni workflow ends with a safety-net cleanup job that always runs and deletes the known system-test clusters remaining in Omni. It uses the action at .github/actions/cleanup-omni/action.yaml and is not intended for local execution.
| Workflow | Schedule | Purpose |
|---|---|---|
update-skills |
Daily (06:00 UTC) | Copilot skills upgrades |
maintenance |
Daily (00:00 UTC) and monthly (1st, 00:15 UTC) | Old workflow run cleanup; monthly package cleanup |
update-hetzner-talos-iso |
Every 6 hours | Track Hetzner's latest compatible Talos ISO |
system-test-hetzner |
Weekly (Sunday 03:00 UTC) | Hetzner system tests |
system-test-k3k-calico |
Weekly (Monday 02:30 UTC) | Nested Kubernetes provider system test |
codeql |
Weekly (Saturday 06:46 UTC) | Code scanning |
The release process for KSail is fully automated and split across two GitHub Actions workflows:
- Release (
.github/workflows/release.yaml) runs on pushes tomainand creates the next semantic version tag (vX.Y.Z) based on Conventional Commits (typically the PR title / squash-merge commit message). - CD (
.github/workflows/cd.yaml) runs on tag pushes (v*) and uses GoReleaser to build and publish release artifacts, followed by MCP registry publishing, documentation deployment to GitHub Pages, VSCode extension publishing, and a Homebrew tap PR.
Versioning conventions:
- fix: Patch release (e.g. 1.0.1)
- feat: Minor release (e.g. 1.1.0)
- BREAKING CHANGE or
!: Major release (e.g. 2.0.0)
The changelog is generated by GoReleaser from the commit history, so keep PR titles and commit messages clear and descriptive.
The CD workflow implements an atomic publication strategy to ensure users never see incomplete releases with missing artifacts:
-
Draft Creation: GoReleaser creates a draft release (configured in
.goreleaser.yaml) with:- Compiled binaries for multiple platforms (Darwin arm64, Linux/Windows on amd64/arm64)
- Docker images published to GHCR
- Generated changelog from commit history
GoReleaser also opens a separate draft PR to update the Homebrew cask in
devantler-tech/homebrew-tap(branch:goreleaser/ksail). -
Other artifacts: Separate jobs build the VSCode extension and the desktop apps, which are attached to the same draft release. Two more jobs publish the operator chart to the container registry and build the documentation site for GitHub Pages; they are not attached to the release, but publication waits for them too.
-
Atomic Publication: A final
publish-releasejob waits for all of those jobs to complete successfully, then publishes the draft release.
This workflow ensures that:
- Releases are only published after all artifacts are uploaded
- Users never encounter partial releases with missing binaries or extensions
- If publication is blocked, the
cleanup-failed-releasejob deletes the unpublished draft and then tries to delete its tag. If the tag cannot be deleted the job fails and names the manual cleanup. When a published release already exists for the tag, it leaves the draft and the tag for a maintainer to inspect