Skip to content

Latest commit

 

History

History
451 lines (305 loc) · 31.5 KB

CONTRIBUTING.md

File metadata and controls

451 lines (305 loc) · 31.5 KB

Contributing

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.

License

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

Your First Contribution

New to KSail? Welcome! Here's how to get started:

  1. Find an issue — Look for issues labeled good first issue for beginner-friendly tasks, or help wanted for tasks where the maintainers are looking for help.
  2. Fork & clone — Fork the repository and clone it locally.
  3. Set up your environment — Follow the Prerequisites section below.
  4. Make your changes — Create a branch, implement your fix or feature, and write tests.
  5. Submit a PR — Open a pull request against main with a clear description of what changed and why.

If you have questions, don't hesitate to ask in GitHub Discussions.

The smallest safe first change

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 PR

A 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.

Getting Started

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

Code Documentation

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.

Prerequisites

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 the eksctl binary on your PATH; 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 go line of go.mod or 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 plain go build does not need it.
  • GNU coreutils — The CI harness uses GNU timeout to test hanging processes. On macOS, install coreutils; the harness also recognizes its gtimeout command.

Code Style

Follow standard Go conventions in this repository:

  • Formatting: Format code with gofmt and keep imports organized with goimports (or run golangci-lint run --fix). Keep lines under 100 characters (enforced by golines).
  • Naming: Exported identifiers use PascalCase; unexported identifiers use camelCase. 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 panic in library code. Wrap errors with context using fmt.Errorf("context: %w", err). All errors must be checked (golangci-lint enforces this).
  • Testing: Prefer table-driven tests with t.Parallel(). Keep tests focused on public behavior. Run go test ./... before opening a PR.
  • Path safety: Canonicalize all user-supplied file and directory paths with fsutil.EvalCanonicalPath before use (resolves symlinks, prevents path-escape attacks). Prefer fsutil.ReadFileSafe for constrained reads instead of reimplementing path-containment checks. For output paths that may not exist yet, create the parent directory with os.MkdirAll first, 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.).

Lint

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 go

The 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).

Build

# 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-optimized

The 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 -w flags strip debug symbols (reducing binary size significantly) and the -X flags inject version metadata via GoReleaser. Development builds retain debug symbols for a better debugging experience.

Native desktop builds

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 lint

Linux 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.

Test

Generating mocks

# working-directory: ./
mockery

Tip: make generate runs mockery together with every other generator in the repository — see Auto-generated artifacts.

Unit tests

# working-directory: ./
go test ./...

Documentation

The project documentation is built using Astro with the Starlight theme and is located in the docs/ directory.

Auto-generated artifacts

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 generate

The 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/ and pkg/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 run go generate ./docs/... first)
  • Mocks: mocks.go files — mockery
  • Web UI config types: web/ui/src/generated/ksail-config.ts — npm --prefix web/ui run gen:types (reads the JSON schema, so run go 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.

Building the documentation

# 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 site

The built site will be available in docs/dist/. Note that docs/dist/ and docs/node_modules/ are excluded from git via .gitignore.

VSCode Extension

The VSCode extension is located in the vsce/ directory and provides cluster management capabilities directly in VSCode.

Building the extension

# 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

Testing locally

  1. Open the vsce folder in VSCode
  2. Press F5 to launch Extension Development Host
  3. 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.

Project Structure

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 via pkg/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

Key Packages in pkg/

  • 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 by ksail open web and 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.Provider for 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

Architecture: Providers vs Provisioners

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.

CI

GitHub Workflows

Unit Tests

# working-directory: ./
go test ./...

System Tests

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-test path list in the changes job of ci.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=true

This 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.

Hetzner Provider Testing

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.

Omni Provider Testing

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 via spec.provider.omni.serviceAccountKeyEnvVar in ksail.yaml.
  • Omni endpoint – The URL of your Sidero Omni instance. Set it in the OMNI_ENDPOINT environment variable (the name is configurable via spec.provider.omni.endpointEnvVar) or as spec.provider.omni.endpoint in ksail.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.

Scheduled Workflows

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

CD

Release Process

The release process for KSail is fully automated and split across two GitHub Actions workflows:

  1. Release (.github/workflows/release.yaml) runs on pushes to main and creates the next semantic version tag (vX.Y.Z) based on Conventional Commits (typically the PR title / squash-merge commit message).
  2. 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.

Atomic Draft Release Workflow

The CD workflow implements an atomic publication strategy to ensure users never see incomplete releases with missing artifacts:

  1. 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).

  2. 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.

  3. Atomic Publication: A final publish-release job 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-release job 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