Contributing

This document is the single source of truth for contribution guidelines.

Toolchain#

Prerequisites#

  • Go 1.25+ (the project uses go tool for managed tool dependencies)

Common commands#

CommandDescription
go buildBuild the gck binary
go run . createBuild and run in one step
task testRun all tests (gotestsum)
task lintRun Go linter (revive)
task fmt:yamlFormat YAML files in registry/ and root
task lint:yamlCheck YAML formatting (CI-safe, no writes)
task validateValidate all registry gck.yaml files against the schema
task site:generateGenerate Hugo site content from the registry
task site:serveGenerate content and start the Hugo dev server
task site:buildGenerate content and build the Hugo site for production

When adding or modifying tasks in taskfile.yml, please update the table above to keep it in sync.

Always run go tool task lint and go tool task test before submitting any change.

The generated files under site/content/registry/ and site/static/ are gitignored and rebuilt by CI. When your changes touch files under registry/ or site/ (layouts, templates, CSS, config, the generator itself), run task site:serve to verify the site renders correctly.

Registry card ordering#

On the registry page, cards are displayed in a flat grid with same-org contexts grouped together. By default, orgs are sorted alphabetically by their first path segment (e.g. elastic, kafka, gravitee-io).

To pin an org to the top of the list, add its first path segment to the pinnedGroups array in site/hugo.toml:

[params]
  pinnedGroups = ["gravitee-io"]

Cards from pinned orgs are rendered first (in the order listed), followed by the remaining orgs alphabetically.

Commit conventions#

This project follows Conventional Commits .

Format#

<type>: <subject>
  • type — one of feat, fix, docs, refactor, style, test, chore
  • subject — lowercase, imperative mood, no trailing period

Examples#

feat: add DNS wildcard support
fix: expand env vars in fromFile paths
docs: document values deep merge behavior
refactor: reorganize APIM registry under oss/ee groups
style: improve terminal output

Rules#

  • Keep the subject line concise (≤72 characters).
  • Use a blank line before the body when a longer explanation is needed.
  • Do not add AI-attribution footers (e.g. “Generated by…” or “Co-authored-by: AI”).

Registry structure#

The registry is a tree of context directories under registry/, following the convention org/edition/product/variant:

registry/
├── elastic/
│   └── elasticsearch/          # standalone context
├── gravitee-io/
│   ├── oss/
│   │   ├── am/
│   │   │   ├── base/           # abstract shared config
│   │   │   ├── mongodb/        # concrete variant
│   │   │   └── jdbc/
│   │   │       ├── base/       # abstract JDBC config
│   │   │       ├── postgres/   # concrete variant
│   │   │       └── mysql/      # concrete variant
│   │   ├── apim/
│   │   │   ├── base/           # abstract shared config
│   │   │   ├── dbless/
│   │   │   ├── gateway/
│   │   │   ├── mongodb/        # concrete variant
│   │   │   ├── jdbc/
│   │   │   │   ├── base/       # abstract JDBC config
│   │   │   │   ├── postgres/   # concrete variant
│   │   │   │   ├── mysql/      # concrete variant
│   │   │   │   └── mssql/      # concrete variant
│   │   │   └── opensearch/
│   │   │       ├── base/       # abstract OpenSearch config
│   │   │       └── mongodb/    # concrete variant
│   │   └── gko/                # standalone GKO deployment
│   └── ee/
│       ├── apim/
│       │   ├── base/           # abstract EE config (flags: Kafka, Alert Engine)
│       │   ├── mongodb/        # concrete variant
│       │   └── jdbc/
│       │       ├── postgres/   # concrete variant
│       │       ├── mysql/      # concrete variant
│       │       └── mssql/      # concrete variant
│       └── edge-stack/         # Ambassador Edge Stack
├── kafka/
│   └── standalone/
├── keycloak/
│   └── standalone/
├── mongodb/
│   └── standalone/
├── mysql/
│   └── standalone/
└── postgresql/
    └── standalone/

Files in a context directory#

FilePurpose
gck.yamlComponent definitions, Helm repos, Kind config, features, images
gck--{flag}.yamlContext flag patch file (optional, see Context flags )
README.mdHuman-readable documentation with Hugo YAML frontmatter
notes.createEndpoints and instructions merged into what gck create prints (see notes.create )
.defaultPoints to the default child variant (one variant name per file)

.default files#

When a user specifies a partial path (e.g. gravitee-io/oss/apim), gck walks .default files to resolve the full path. Each .default file contains a single line with the name of the default child directory:

# registry/gravitee-io/oss/apim/.default
postgres

Authoring a good context#

README.md#

Write a README.md with YAML frontmatter and clear sections:

---
title: "Product Name - Variant"
description: "One-line summary of what this context deploys"
tags: [relevant, tags]
---

Every concrete context README must include the following sections in order: Install gck, Usage, Quick Start, and optionally Endpoints.

The Install gck section tells newcomers how to get started:

## Install gck

```bash
go install github.com/gravitee-io-labs/gck@latest
```

For other installation methods, see [Installation](https://gravitee-io-labs.github.io/gck/docs/getting-started/installation/).

If the context uses DNS (directly or via a parent with features.dns.enabled: true), append the DNS setup instructions to the same Install gck section — do not create a second one. Explain that gck setup dns must be run after creating the cluster, that it may require sudo, and link to the Networking guide .

The Usage section must include both the gck create command under a ### Create subtitle and a ### Cleanup block showing gck delete:

## Usage

### Create

```bash
gck create --from <context-path>
```

### Cleanup

```bash
gck delete
```

Concrete context READMEs must be self-contained. A reader who has never used gck should be able to go from zero to a running cluster using only the README. Abstract contexts don’t get pages on the documentation site, so linking to a parent abstract README produces broken links. Inline any relevant documentation from the abstract parent directly into each concrete variant’s README.

Tags#

  • Every context README must have at least one tag.
  • Tags come from a fixed vocabulary defined in registry/tags.yaml:
TagDescription
databasePersistent data stores
messagingMessage brokers and event streaming
networkingAPI gateways, proxies, ingress, service mesh
observabilityLogging, metrics, tracing
searchSearch and indexing engines
securitySecrets management, auth, certificates
sinkTest endpoints for email, webhooks, and other outputs
aiLLM, MCP, A2A
  • gck validate --tags registry/tags.yaml enforces this vocabulary; CI rejects unknown tags.
  • Do not use product or organization names as tags (e.g. kafka, mongodb, gravitee, elasticsearch). Tags describe what the context does, not which product it uses — the registry path already encodes the product.

Icon#

Place an icon.svg file in the context directory (or any ancestor directory) to display a product logo on registry cards and detail pages.

  • SVG format, square aspect ratio recommended.
  • Icons inherit from parent directories: a single icon.svg at registry/org/ covers all contexts under that path.
  • When no icon is found anywhere in the ancestor chain, a generic fallback icon is rendered.

notes.create#

notes.create is what the user sees when gck create finishes. It has two parts: YAML front matter listing the endpoints this context exposes, and a free-form body below it.

---
title: PostgreSQL
endpoints:
  - name: PostgreSQL
    url: localhost:30432
---
Database   gravitee
User       postgres
Password   postgres

  PGPASSWORD=postgres psql -h localhost -p 30432 -U postgres -d gravitee

Notes are merged across every composed context, not overridden. Composing three contexts produces one endpoints table with every row in it, followed by each context’s body under its own title. This is why a context declares only what it owns:

  • Write the note where the port is mapped, including in an abstract base. gravitee-io/oss/apim/base declares the APIM rows once; apim/jdbc/postgres has no notes.create at all and still prints them, alongside PostgreSQL’s and Elasticsearch’s.
  • Do not repeat a parent’s endpoints, and do not open with “Your cluster is ready” – gck prints that line itself.

Declaring in an abstract base pays twice. Besides feeding the terminal output of gck create, the front matter is the source for the Endpoints table on every concrete variant’s registry page: the site generator walks the same from chain and attributes each row to the context that declared it, in a From column. A when-guarded row is listed too, marked with the flag that reveals it – the generator works that out by re-resolving the table with each flag left out, so you never declare the flag name twice. Abstract contexts get no page of their own, so the base is where one declaration reaches the most readers. Never hand-write an endpoints table in a README – the registry page generates it, the same way it generates the Context flags and Variables tables.

Endpoint fields#

FieldPurpose
nameLabel shown in the table, and the merge key
urlAddress, with or without a scheme (localhost:30432, http://localhost:30080)
noteOptional trailing hint (dev mode, security disabled)
whenOptional template expression; the row appears only when it renders truthy

name is how rows merge, so it must stay unambiguous in any stack the context can appear in. APIM and AM both used to call their management API “Management API”; composing them collapsed the two into one row, which is why they are now “APIM API” and “AM API”.

A later context replaces an inherited row by declaring the same namegravitee-io/ee/gamma restates AM Console with a hostname instead of a host port. Setting when: false on the replacement hides the row instead, which is how gravitee-io/oss/apim/dbless suppresses the console and portal it does not deploy. Prose bodies work the same way, keyed by title.

Conditional endpoints#

when and the body are both rendered as Go templates with hasFlag, which returns true when the user passed the named context flag :

endpoints:
  - name: APIM Console
    url: http://localhost:30080
    when: '{{ not (hasFlag "disable-ui") }}'

hasFlag always returns false during gck delete, which has no flag context.

Keeping notes in sync with the config#

The endpoints table is declared, not derived, so task test enforces that it matches the config in both directions:

  • every localhost endpoint must correspond to a hostPort the composition actually maps, so a note cannot outlive the port it documents;
  • every hostPort a context maps itself – in its gck.yaml or in one of its gck--{flag}.yaml patches – must be documented somewhere in its composition. Adding a port and forgetting the note fails the build.

The second rule applies only to contexts that document a localhost endpoint of their own. A context that deliberately routes through hostnames or a load balancer instead maps ports without presenting them as localhost URLs and is left alone.

When you add or move a port, do both: update gck.yaml (or the gck--{flag}.yaml patch) and update the notes.create front matter. The README needs no change – its endpoints table is generated.

Port allocation#

Each product uses a dedicated NodePort range to avoid collisions when composing multiple products or running them side by side:

Product / ContextNodePort range
APIM30080–30085
AM30090–30093
Consul30500
Grafana30300
FastMCP test server30800
fast-time-server30801
Keycloak30880
llm-d inference sim30802
MockServer31080
OTLP (collector, Tempo, Jaeger)30317 (gRPC) / 30318 (HTTP)
Prometheus30909
Tempo (query API)30200
Standalone databases30000 + standard port (e.g. PostgreSQL 30432, MySQL 30306, MongoDB 30017, MSSQL 31433)

When adding a new product, pick the next available range and document it here. Last digits should match the internal container port where practical (e.g. AM Gateway listens on 8092 → NodePort 30092).

Kind cluster name#

Gravitee product contexts set kind.name to gravitee-{product} (e.g. gravitee-apim, gravitee-am). Variants inheriting from a base via from automatically get the base’s kind.name, which is typically correct — only override it if the variant needs a separate cluster.

Images#

  • Use slim base images (Alpine, distroless) whenever possible.
  • Pin image tags — avoid :latest in production contexts.
  • List images in images.preload.refs so they are pulled into the Kind node before Helm installs, reducing startup time:
images:
  preload:
    refs:
      - org/image:tag
  • Use images.preload.mode: replace when a variant deploys a strict subset of the parent’s components and the inherited preload list includes images that are no longer needed. This drops the inherited refs and uses only the ones declared in the current context:
images:
  preload:
    mode: replace
    refs:
      - org/subset-image:tag

Startup optimization#

  • Use minimal resource requests suitable for local development.
  • Disable persistence for dev-oriented setups (e.g. persistence.enabled: false).
  • Disable unnecessary features to reduce resource usage and startup time.

Composition with from#

Reuse existing building blocks (databases, message brokers) instead of duplicating configuration. The from field composes one or more parent contexts:

from:
  - mongodb/standalone
  - elastic/elasticsearch/standalone
  - gravitee-io/oss/apim/base

When a child needs a different default for a parent’s variable (e.g. a different image tag), use a path-scoped override in the child’s vars block instead of duplicating the parent’s manifests. See the Template variables section for syntax.

Abstract contexts#

Use abstract: true for shared base configurations that should not be deployed directly.

Abstract contexts are meant to be composed into concrete variants via from:

abstract: true

helm:
  repos:
    - name: graviteeio
      url: https://helm.gravitee.io

components:
  - name: apim
    type: helm
    namespace: gravitee
    helm:
      chart: graviteeio/apim
      values:
        # shared defaults...

Dependencies between components#

Declare requires with conditions.ready: true when a component depends on another being fully running before it can start:

components:
  - name: apim
    requires:
      - component: mongodb
        conditions:
          ready: true
        selector:
          matchLabels:
            app: mongodb
        timeout: 15m

Helm values#

  • Use valueFiles for large Helm value overrides that would clutter inline YAML.
  • Use inline values for small, context-specific tweaks.

Optional resources#

Use onMissing: ignore for optional resources like license keys that may not exist on every developer’s machine:

components:
  - name: license
    type: k8s
    namespace: gravitee
    k8s:
      secrets:
        - name: gravitee-license
          fromFile: '{{ env "HOME" }}/opt/gravitee/license.key'
          onMissing: ignore

Context flags#

Context flags let users customize a deployment without requiring a separate context directory for every combination. Define a flag by creating a gck--{flag-name}.yaml patch file alongside the context’s gck.yaml:

# gck--disable-portal.yaml
description: "Disable the developer portal UI"
components:
  - name: apim
    helm:
      values:
        portal:
          enabled: false

Naming convention: flag names must be lowercase kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$). The description field is required and is displayed on the registry site and in validation output. Use the disable- prefix for flags that remove a default component (e.g. --disable-es, --disable-portal) and the enable- prefix for flags that add an optional component that is off by default (e.g. --enable-hc-vault).

Inheritance: flags placed on an abstract context are automatically inherited by every concrete context that composes from it via from. A child can override an inherited flag by providing its own file with the same name.

Flags can also fully exclude a component from deployment by setting enabled: false. Any requires entries referencing a disabled component are silently dropped:

components:
  - name: elasticsearch
    enabled: false

When to use flags vs separate contexts:

  • Can a user toggle this on or off without changing the stack’s identity? Use a flag. Examples: --disable-es disables Elasticsearch, --disable-portal hides the portal UI, --enable-hc-vault adds a HashiCorp Vault instance. The stack is still “APIM with Postgres” regardless.
  • Does this change the storage backend, networking model, or deployment topology? Use a separate context directory. Examples: mongodb/ vs jdbc/ (different persistence models), dbless/ vs gateway/ (fundamentally different gateway modes).
  • Is there shared config used by multiple sibling variants? Extract it into an abstract base (abstract: true) and have variants compose from it via from. Example: oss/apim/base/ holds the shared Helm repo, component skeleton, and port mappings; oss/apim/jdbc/base/ adds JDBC persistence; oss/apim/jdbc/postgres/ and oss/apim/jdbc/mysql/ extend it with database-specific config.
  • Does a feature layer apply across multiple existing contexts? Create a composable abstract context that stacks on top via from, using flags for optional features. Example: ee/apim/base/ adds Kafka Gateway and license handling with flags for Alert Engine; ee/apim/mongodb/ and ee/apim/jdbc/postgres/ compose it with the corresponding OSS DB context.
  • When in doubt: prefer a flag. Flags are cheaper to add, don’t create new directories, and inherit automatically through from. A flag can always be promoted to a separate context later if the divergence grows.

Users activate flags on the command line:

gck create --from gravitee-io/oss/apim --disable-portal --enable-hc-vault

Template variables#

gck.yaml files support Go text/template expressions. Declare variable defaults in a top-level vars block and reference them with {{ .variableName }}:

vars:
  helmVersion: ""
  imageTag: "latest"

components:
  - name: apim
    helm:
      chart: graviteeio/apim
      version: "{{ .helmVersion }}"
      values:
        gateway:
          image:
            tag: "{{ .imageTag }}-debian"

Users override defaults at deploy time with --set:

gck create --set imageTag=4.12.0

Path-scoped overrides#

A child context can override a parent’s variables by nesting them under the parent’s registry path segments in its own vars block:

# gravitee-io/oss/am/jdbc/mysql/gck.yaml
from:
  - mysql/standalone
  - gravitee-io/oss/am/jdbc/base

vars:
  jdbcDriver:
    default: "mysql"

  # Override mysql/standalone's imageTag
  mysql:
    standalone:
      imageTag:
        default: "8"

The disambiguation rule: an entry with a default key is a var declaration; an entry without is a path segment leading to overrides.

Scoped --set#

Dotted --set keys target a specific context in the composition chain. The system matches dots against known context paths using longest-prefix matching:

# Override mysql/standalone's imageTag only
gck create --from gravitee-io/oss/am/jdbc/mysql --set mysql.standalone.imageTag=8.4

# Broadcast to all contexts declaring imageTag
gck create --from gravitee-io/oss/am/jdbc/mysql --set imageTag=4.6.0

Conventions#

  • Variable names use camelCase (imageTag, helmVersion).
  • Variable names must not contain dots.
  • Registry path segments must not contain dots (enforced by gck validate).
  • The vars block must not itself contain template expressions.
  • Undefined variables with no default cause a clear error (missingkey=error).

Standard variable names#

VariableControlsDefault
helmVersionhelm.version (chart version)"" (latest)
imageTagImage tags in helm values / manifests"latest"
imageRepositoryImage repository (optional)chart default

Template functions#

FunctionUsageDescription
env{{ env "HOME" }}Returns the value of an environment variable.
default{{ .myVar | default "fallback" }}Returns the fallback when the pipeline value is empty.
required{{ .myVar | required "msg" }}Returns the value or fails with the given message when empty.

Environment variables#

The env template function replaces the old $VAR / ${VAR} shell expansion that was previously supported in fromFile paths and build fields. Use {{ env "HOME" }} instead of $HOME:

builds:
  - name: gateway
    image: graviteeio/apim-gateway:latest
    dir: '{{ env "HOME" }}/src/gravitee'

Scope#

Templating applies to every gck.yaml in the pipeline: user config, $GCK_HOME/gck.yaml, patch files, flag overlays, and registry context files. Each context is rendered with its effective vars: own defaults, overridden by child path-scoped overrides, overridden by --set (broadcast then scoped). Templates use short names ({{ .imageTag }}) – a context’s templates can only access its own vars.

Schema#

The JSON Schema for gck.yaml lives at schema/gck.schema.yaml.

Validate your context files against it with gck validate (or go run . validate) and always keep it in sync when modifying config structs in internal/config/.

# Validate a single file
gck validate registry/kafka/standalone/gck.yaml

# Validate all contexts in the registry
gck validate registry/