This document is the single source of truth for contribution guidelines.
Toolchain#
Prerequisites#
- Go 1.25+ (the project uses
go toolfor managed tool dependencies)
Common commands#
| Command | Description |
|---|---|
go build | Build the gck binary |
go run . create | Build and run in one step |
task test | Run all tests (gotestsum) |
task lint | Run Go linter (revive) |
task fmt:yaml | Format YAML files in registry/ and root |
task lint:yaml | Check YAML formatting (CI-safe, no writes) |
task validate | Validate all registry gck.yaml files against the schema |
task site:generate | Generate Hugo site content from the registry |
task site:serve | Generate content and start the Hugo dev server |
task site:build | Generate 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#
| File | Purpose |
|---|---|
gck.yaml | Component definitions, Helm repos, Kind config, features, images |
gck--{flag}.yaml | Context flag patch file (optional, see Context flags ) |
README.md | Human-readable documentation with Hugo YAML frontmatter |
notes.create | Endpoints and instructions merged into what gck create prints (see notes.create
) |
.default | Points 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:
| Tag | Description |
|---|---|
database | Persistent data stores |
messaging | Message brokers and event streaming |
networking | API gateways, proxies, ingress, service mesh |
observability | Logging, metrics, tracing |
search | Search and indexing engines |
security | Secrets management, auth, certificates |
sink | Test endpoints for email, webhooks, and other outputs |
ai | LLM, MCP, A2A |
gck validate --tags registry/tags.yamlenforces 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.svgatregistry/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/basedeclares the APIM rows once;apim/jdbc/postgreshas nonotes.createat 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#
| Field | Purpose |
|---|---|
name | Label shown in the table, and the merge key |
url | Address, with or without a scheme (localhost:30432, http://localhost:30080) |
note | Optional trailing hint (dev mode, security disabled) |
when | Optional 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 name –
gravitee-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
localhostendpoint must correspond to ahostPortthe composition actually maps, so a note cannot outlive the port it documents; - every
hostPorta context maps itself – in itsgck.yamlor in one of itsgck--{flag}.yamlpatches – 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 / Context | NodePort range |
|---|---|
| APIM | 30080–30085 |
| AM | 30090–30093 |
| Consul | 30500 |
| Grafana | 30300 |
| FastMCP test server | 30800 |
| fast-time-server | 30801 |
| Keycloak | 30880 |
| llm-d inference sim | 30802 |
| MockServer | 31080 |
| OTLP (collector, Tempo, Jaeger) | 30317 (gRPC) / 30318 (HTTP) |
| Prometheus | 30909 |
| Tempo (query API) | 30200 |
| Standalone databases | 30000 + 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
:latestin production contexts. - List images in
images.preload.refsso they are pulled into the Kind node before Helm installs, reducing startup time:
images:
preload:
refs:
- org/image:tag
- Use
images.preload.mode: replacewhen 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
valueFilesfor large Helm value overrides that would clutter inline YAML. - Use inline
valuesfor 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-esdisables Elasticsearch,--disable-portalhides the portal UI,--enable-hc-vaultadds 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/vsjdbc/(different persistence models),dbless/vsgateway/(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 viafrom. 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/andoss/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/andee/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
varsblock must not itself contain template expressions. - Undefined variables with no default cause a clear error
(
missingkey=error).
Standard variable names#
| Variable | Controls | Default |
|---|---|---|
helmVersion | helm.version (chart version) | "" (latest) |
imageTag | Image tags in helm values / manifests | "latest" |
imageRepository | Image repository (optional) | chart default |
Template functions#
| Function | Usage | Description |
|---|---|---|
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/