Skip to main content

Environment

API Group: environments.blanketops.dev

Version: v1alpha1

Scope: Namespaced


Description

Environment is the envelope of the delivery chain — a versioned, isolated execution context where an application runs.

It composes GitRepository, GitHubEvent, Build, Package, ServiceUnit, Deployment, Route, and Domain by reference, and owns them via ownerReference (cascade delete).

  • Environment does not build artifacts.
  • Environment does not deploy workloads.
  • Environment does not route traffic.
  • Environment aggregates and owns the resources that make up one application delivery.

Spec

spec


FieldTypeRequiredDescription
contractobjectYesEnvironment composition contract

spec.contract

FieldTypeRequiredDescription
applicationNamestringYesHuman-readable application identifier
branchstringYesGit branch associated with this environment
gitOwnerstringYesOwning Git organisation or user
environmentTypestringYesdevelopment, staging, production, or testing
versionstringNoApplication version, semantic or otherwise
descriptionstringNoHuman-readable description
gitRepositoryobjectNoGitRepository composed into this environment
gitHubEventobjectNoGitHubEvent composed into this environment
buildobjectNoBuild composed into this environment
packageobjectNoPackage composed into this environment
serviceUnits[]objectNoServiceUnits composed into this environment
deploymentobjectNoDeployment composed into this environment
routeobjectNoRoute composed into this environment
domainobjectNoDomain composed into this environment
contractobjectNoPlatform-level bindings — see Secrets & SecretStore below

spec.contract composed refs (gitRepository, gitHubEvent, build, package, deployment, route, domain, serviceUnits[])

Each is an object reference, resolved in the same namespace as the Environment — a composition pointer, not an embedded contract.

FieldTypeRequiredDescription
namestringYesName of the referenced CR

Status

FieldTypeDescription
phasestringCurrent aggregate lifecycle phase
messagestringHuman-readable status summary
conditions[]ConditionPer-resource readiness conditions, e.g. BuildReady, DeploymentReady, RouteReady
lastUpdatedAtstringTimestamp when status was last updated

status.phase Values

ValueMeaning
PendingOne or more composed resources are not yet ready
ReadyAll composed resources are ready
DegradedThe environment is serving but one or more resources are unhealthy
FailedOne or more composed resources failed

Secrets & SecretStore

Every credential consumed downstream — Git SSH keys, registry push credentials, the GitHub webhook HMAC secret — is pulled from an external secrets backend via External Secrets Operator (ESO), never created by hand and never inlined into a CR. The chain is: an environment operator's own secret manager → a ClusterSecretStore ESO can read → a per-resource ExternalSecret the platform materializes → the plain Secret a workload actually mounts.

Order matters: the ClusterSecretStore and every key it needs to hold must exist before the Environment is created, not after. Environment is the resource that resolves contract.secretStore.provider in the first place — Build, Deployment, Package, and GitHubEvent all mediate off an Environment that's already composed them, and each expects its own credential to already be resolvable at that point. Create the Environment before its secrets exist, and mediation of every resource that needs one fails — not just the one missing a key.

spec.contract.contract.secretStore.provider is the one input that drives all of it. Yes, contract nested inside contract — the outer one is the Kubernetes envelope every resource uses (spec.contract), the inner one is EnvironmentContract, the Environment resource's own platform-level bindings field.

FieldTypeRequiredDescription
contract.secretStoreobjectYes, if contract is setWhich external secrets backend backs this environment
contract.secretStore.providerstringYesaws, vault, gcp, or azure

provider resolves to a fixed ClusterSecretStore name — you don't choose the name yourself:

providerClusterSecretStore name
awsblanketops-environments-aws
vaultblanketops-environments-vault
gcpblanketops-environments-gcp
azureblanketops-environments-azure
(unset/unknown)falls back to a fake store — fine for a local Kind cluster, not for anything that needs real credentials

That ClusterSecretStore is infrastructure you (or whoever owns the cluster) create once, pointed at your actual AWS Secrets Manager / Vault / GCP Secret Manager / Azure Key Vault instance. It is not something the Environment controller creates for you. Minimal AWS example:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: blanketops-environments-aws
spec:
provider:
aws:
service: SecretsManager
region: af-south-1
auth:
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secrets

Full worked examples for AWS, Azure, and GCP ship as CRD samples in environments-install.

Once that store exists, each composed CR materializes its own ExternalSecret against it, at a fixed, platform-constant remote path — these paths are not configurable per-environment, and need a value in your secret backend before the corresponding CR is applied:

Remote keyBacksMaterialized Secret key(s)Secret type
/blanketops/git/ssh-privatekeyBuild's source clone, Deployment's manifests-repo clone, Package's state-repo clonessh-privatekey (ssh_privatekey for Package)kubernetes.io/ssh-auth
/blanketops/git/ssh-publickeysame three consumersssh-publickey (ssh_publickey for Package)
/blanketops/git/known-hostssame three consumersknown_hosts
/blanketops/registry/configBuild's registry push, Package's registry credentials.dockerconfigjson (dockerconfigjson for Package)kubernetes.io/dockerconfigjson
/blanketops/github/webhook/secretGitHubEvent's webhook signature (spec.webhook.secretRef)whatever key name webhook.secretRef.key declaresOpaque
/blanketops/crossplane/github/tokenCrossplane's GitHub provider (repository/webhook provisioning)tokenOpaque
/blanketops/github/api/tokenListed by the install-repo secret store samples as a required path; no reconciler reads it yet

Two secrets in this chain are not ESO-sourced, and need no key in your backend at all:

  • Flux's Git SSH keypair (<deployment-name>-flux-ssh) — generated by the controller itself, once per Deployment, and left alone afterward.
  • The webhook hook-URL secret (<repository-name>-hookurl) — copied directly from GitRepository.spec.contract.hookUrl, a user-declared field, not a secret-store lookup.

The rest, all six, need a value in your backend before the Environment is created. For AWS Secrets Manager, that's:

aws secretsmanager create-secret --name /blanketops/git/ssh-privatekey        --secret-string "$(cat ~/.ssh/id_ed25519)"
aws secretsmanager create-secret --name /blanketops/git/ssh-publickey --secret-string "$(cat ~/.ssh/id_ed25519.pub)"
aws secretsmanager create-secret --name /blanketops/git/known-hosts --secret-string "$(ssh-keyscan github.com)"
aws secretsmanager create-secret --name /blanketops/registry/config --secret-string "$(cat ~/.docker/config.json)"
aws secretsmanager create-secret --name /blanketops/github/webhook/secret --secret-string "<your-github-webhook-hmac-secret>"
aws secretsmanager create-secret --name /blanketops/crossplane/github/token --secret-string "<a-github-token-with-repo-and-webhook-scope>"

Vault, GCP, and Azure follow the same six paths — see the provider-specific samples linked above for the exact command per backend (GCP flattens / to _, Azure flattens it to --).

Once every key above resolves through the ClusterSecretStore, create the Environment. The controller reconciles every ExternalSecret (and the Secret ESO materializes from it) automatically from there; there is nothing to kubectl create secret by hand.


Example

apiVersion: environments.blanketops.dev/v1alpha1
kind: Environment
metadata:
name: for-kaniko-app-main
namespace: dev
spec:
contract:
applicationName: for-kaniko-app
branch: main
gitOwner: example-org
environmentType: development
version: v0.1.0
gitRepository:
name: for-kaniko-app
gitHubEvent:
name: for-kaniko-app-3f2c91d
build:
name: build-sample-kaniko
serviceUnits:
- name: for-kaniko-app-api
deployment:
name: for-kaniko-app
route:
name: route-sample
domain:
name: for-kaniko-app-domain
contract:
secretStore:
provider: aws

Invariants

  • applicationName, branch, and gitOwner together identify the environment — combinations should not collide within a namespace.
  • Every resource referenced under the contract must exist in the same namespace as the Environment.
  • Deleting an Environment cascades to every resource it owns via ownerReference.
  • phase is derived from the aggregate readiness of composed resources — it is never set directly.
  • If contract is set, contract.secretStore.provider is required.