Skip to content

FUN-5 Identifiers

Committed

1. Introduction

Every resource in Fundament has two kinds of identity: a UUIDv7 primary key for internal use and a human-readable name for external use. This document describes the naming constraints for human-readable names and how they interact with Kubernetes.

The constraints aren’t arbitrary. They exist because Fundament resources map to Kubernetes resources, and Kubernetes has strict rules about what constitutes a valid name. Getting this wrong means a project that works fine in our API but can’t be provisioned on the cluster.

2. General Rules

Names are immutable after creation. This is a deliberate choice: names appear in Kubernetes resources, DNS records, URLs, and configuration files. Renaming a resource would require cascading changes across all of those, which is fragile and error-prone. If you need a different name, create a new resource.

Where resources interact with Kubernetes, names follow the DNS-1123 label specification: lowercase alphanumeric characters and hyphens, starting with a letter, ending with an alphanumeric character, at most 63 characters. This is the most restrictive common denominator across Kubernetes resource types.

3. Entity-Specific Constraints

3.1. Organization Name

The organization name is the most constrained identifier. It serves as the tenant identifier in URLs, API paths, and potentially DNS names.

  • Regex: ^[a-z][a-z0-9-]*[a-z0-9]$

  • Uniqueness: globally unique (soft-deleted names can be reused)

  • Immutable after creation

This regex is slightly more permissive than DNS-1123 (it doesn’t enforce the 63-character limit), but in practice organization names are short. Organizations also have a mutable alias (1-255 characters) for human-readable presentation.

3.2. Cluster Name

  • Length: 1-255 characters

  • Uniqueness: unique per organization

  • No regex constraint: cluster names don’t directly become Kubernetes resource names (the Gardener shoot name is derived separately)

3.3. Project Name

  • Regex: ^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$ (DNS-1123 label)

  • Max length: 30 characters

  • No --: two consecutive hyphens are reserved as the separator in Kubernetes namespace names

  • Uniqueness: unique per cluster

Project names are DNS-1123 labels because they form part of the Kubernetes namespace name (see below). The 30-character cap leaves every project at least 27 characters for its namespace names. Projects created before these two rules keep their name.

3.4. Namespace Name

  • Regex: ^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$ (DNS-1123 label)

  • Max length: 57 minus the length of the project name (at least 27)

  • Uniqueness: unique per project

Same constraints as project names, for the same reason; the cluster-side name tnt-<project>--<namespace> must itself fit a 63-character label.

3.5. NodePool Name

  • Length: 1-255 characters

  • Uniqueness: unique per cluster

  • No regex constraint: node pool names are passed to Gardener but don’t appear in DNS

4. Kubernetes Namespace Convention

When a namespace is provisioned on a cluster, the Kubernetes namespace name is a fixed prefix, the project name, a double dash and the namespace name:

tnt-{project}--{namespace}

Each part has a job:

  • tnt- marks every namespace fundament creates for a project (a tenant namespace). Nothing else on a cluster uses this prefix, so a project namespace can never take the name of a system or plugin namespace: without it, project cert with namespace manager would become cert-manager, the namespace the cert-manager plugin installs into, and the project’s admins would get access to its secrets. A list of forbidden names can’t prevent this, because plugins choose their namespaces and may be installed after the project namespace exists.

  • -- separates project and namespace. Project names may not contain it, so two different pairs never produce the same name (a-b + c gives tnt-a-b--c, a + b-c gives tnt-a--b-c).

This is the scheme Gardener uses for the same two problems: project namespaces in the garden must start with garden-, and a shoot’s control-plane namespace is shoot--{project}--{shoot}, with -- forbidden in project and shoot names (gardener#164). Gardener’s name rules apply to its own project and shoot names only; these namespaces live inside the shoot, where the Kubernetes limit of 63 characters is the only one.

Project names are unique per cluster and immutable, so the result is readable in every Kubernetes tool and stable for the namespace’s life. Some examples:

Project Namespace K8s Namespace

burger-test

foo-app

tnt-burger-test--foo-app

burger-test

observability-tools

tnt-burger-test--observability-tools

burger-prod

foo-app

tnt-burger-prod--foo-app

Two rules keep the scheme valid, both enforced when a namespace is created:

  • Length: 4 + len(project) + 2 + len(namespace) must not exceed 63, so a namespace can be at most 57 - len(project) characters.

  • Uniqueness on the cluster: projects created before -- was reserved may contain it (x--y + z and x + y--z both give tnt-x--y--z), so a namespace whose Kubernetes name is already used by another project’s namespace on the cluster is still refused.

Namespaces created before this convention carry a hashed project prefix (<8 chars of project><4 hash chars>-<namespace>). Kubernetes namespaces can’t be renamed, so they keep that name; the cluster-worker identifies every namespace by its fundament.io/namespace-id label, not by its name.

5. Uniqueness Scoping

Names are unique within their parent scope:

  • Organization names are globally unique

  • Cluster names are unique within an organization

  • Project names are unique within a cluster

  • Namespace names are unique within a project

  • NodePool names are unique within a cluster

Soft-deleting a resource releases its name and you can create a new resource with the same name after deleting the old one. Only one active (non-deleted) resource can hold a given name within its scope. See FUN-6 for the soft-delete pattern.

6. Primary Identifiers

UUIDv7 is the canonical identifier for all resources. Names exist for human readability and Kubernetes integration, but the API uses UUIDs for all references (foreign keys, request parameters, etc.). See FUN-6 for the rationale behind UUIDv7.

The API provides GetByName endpoints for convenience (looking up a cluster by name within an organization, for example), but these are sugar on top of the UUID-based model.

  • FUN-4: Organizational hierarchies (what resources exist and how they relate)

  • FUN-6: API-first design (UUIDv7 rationale, soft-delete pattern)