Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,14 +275,15 @@ The spec is a smaller surface than the Kubernetes API on purpose, so step 2 adds
fields you did not write. You can read the result before anything is applied:

```sh
deployah plan <environment> --offline --raw --yaml
deployah plan <environment> --raw --yaml
```

`--offline` renders without touching a cluster, `--raw` prints raw Kubernetes
field paths instead of the compact Deployah vocabulary, and `--yaml` shows
changed fields as YAML blocks. For the resolved hostname, TLS mode, context,
and runtime environment, use `deployah resolve <environment>` (also offline,
`--output json` for machine-readable output).
`deployah plan` renders the chart and compares it with the release on your
cluster, so it needs cluster access. `--raw` prints raw Kubernetes field paths
instead of the compact Deployah vocabulary, and `--yaml` shows changed fields
as YAML blocks. For the resolved hostname, TLS mode, context, and runtime
environment without a cluster, use `deployah resolve <environment>`
(`--output json` for machine-readable output).

For how Deployah compares to similar tools (DevSpace, Werf, Score, Epinio,
Kubero), see [docs/comparison.md](docs/comparison.md).
Expand Down Expand Up @@ -422,7 +423,7 @@ These work with every command:
| `deployah validate <environment>` | Also load the platform file and check the resolved configuration for that environment. |
| `deployah resolve <environment>` | Preview the fully resolved hostname, TLS mode, context, and runtime environment, offline. Prints FileValues and ExplicitValues; do not treat the output as secret-safe CI output. Use `--output json` for machine-readable output. |
| `deployah resolve --environments` | List every environment from both files: where it is registered, its context (or the kubeconfig fallback), domains, and overrides. |
| `deployah plan <environment>` | Inspect changes for an environment, without applying anything. Extra manifests from `.deployah/manifests/` appear in the diff; CRD files from `.deployah/crds/` are listed with Helm's install-only lifecycle, not applied. Use `--offline` to render with no cluster access, `--raw` for raw Kubernetes field paths instead of the compact Deployah vocabulary, `--yaml` to show changed fields as YAML blocks, `--drift` to also compare against live cluster state, `--detailed-exitcode` to exit 2 when changes are pending, or `--output json` for CI. |
| `deployah plan <environment>` | Inspect changes for an environment, without applying anything. It needs cluster access. Extra manifests from `.deployah/manifests/` appear in the diff; CRD files from `.deployah/crds/` are listed with Helm's install-only lifecycle, not applied. Use `--raw` for raw Kubernetes field paths instead of the compact Deployah vocabulary, `--yaml` to show changed fields as YAML blocks, `--drift` to also compare against live cluster state, `--detailed-exitcode` to exit 2 when changes are pending, or `--output json` for CI. |
| `deployah deploy <environment>` | Deploy your project. Deployah validates the spec, runs deploy guards, then always runs a Helm install for a new release or a Helm upgrade for an existing one. Use `--skip-crds` to skip installing [chart CRDs](docs/custom-manifests-and-crds.md#helm-crd-lifecycle) on a fresh Helm install (a CRD added after that first install is not installed by a later deploy), `--explain` to print the resolution report first, `--force-hostname-change` to bypass the hostname guard, or `--resize-volumes` to grow [persistence](docs/workloads.md#growing-volumes) sizes. |
| `deployah run <task> <environment>` | Run a spec task as a one-off Job. Wait is the default; `--detach` returns after create. `--count` / `--parallelism` override fanout for that run. |
| `deployah status <project>` | Show the status of a deployed project. Use `--detailed` for pod details, `-e` for an environment. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ layer, not how streams merge.
done" inside the resolver.
- Tasks cannot inherit from other tasks. Share a component, or repeat
`envFile` / `env:`.
- Invalid keys fail every env-selecting command, including offline plan.
- Invalid keys fail every env-selecting command.
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ resource consequences.
## Decision

A semantic plan requires enough release state to construct Previous
and enough cluster state to construct Live. There is no offline
semantic plan in the target architecture.
and enough cluster state to construct Live. Deployah does not support
offline semantic planning.

When that information is unavailable, the planner must not fabricate
Previous, Live, Drift, HelmAction, or resource consequences.

A Desired-only render or validation capability may exist separately.
Rendering is not semantic planning. This ADR does not freeze a command
name for that capability.
Cluster-independent validation and configuration resolution are
separate capabilities. They are not semantic planning.

## Consequences

Expand All @@ -33,5 +32,5 @@ name for that capability.

### Negative

- Operators without release and cluster access cannot get a semantic
plan. They need the separate render capability.
- Operators without release and cluster access cannot obtain a
semantic plan.
3 changes: 1 addition & 2 deletions docs/cli/deployah_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ deployah plan <environment> [flags]

```text
--detailed-exitcode Exit 2 when the plan has pending changes, 0 when it does not, 1 on error (for CI)
--drift Detect drift between the rendered manifests and the live cluster state (requires cluster access; not compatible with --offline)
--offline Render and validate the chart without contacting the cluster
--drift Detect drift between the rendered manifests and the live cluster state
--output string Output format (default "text")
--raw Show raw Kubernetes field paths instead of the compact Deployah vocabulary
--show-secrets Reveal masked secret values in text output (requires an interactive terminal; refused with --output json)
Expand Down
14 changes: 7 additions & 7 deletions docs/custom-manifests-and-crds.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ spec:

```sh
deployah plan prod # extras appear in the plan diff
deployah deploy prod -y # Helm install processes chart CRDs, then the release
deployah deploy prod # Helm install processes chart CRDs, then the release
```

## Labels and annotations
Expand Down Expand Up @@ -126,12 +126,12 @@ later.
reject invalid or unsupported content.
- An extra that collides with a generated chart object fails the render
(Deployah will not overwrite chart resources).
- Custom resource kinds must be known to Deployah's built-in table or
live cluster discovery. A small offline allowlist covers common
operator APIs (cert-manager and prometheus-operator). Deployah does
not read `.deployah/crds/` to learn a custom resource's type or
scope. With `deployah plan --offline`, unknown kinds are allowed so
you can still preview; scope defaults to namespaced.
- Custom resource kinds must be known to Deployah's built-in scope table
or live cluster discovery. The built-in table includes common operator
APIs such as cert-manager and prometheus-operator. Deployah does not
read `.deployah/crds/` to learn a custom resource's type or scope.
When no REST config is available, unknown kinds are accepted and
default to namespaced.
- Helm hook annotations (`helm.sh/hook`, `helm.sh/hook-weight`,
`helm.sh/hook-delete-policy`) are not supported on custom manifests. Use a
Deployah `preDeploy` or `postDeploy` task for deploy hooks.
Expand Down
4 changes: 0 additions & 4 deletions internal/cmd/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,6 @@ func (s *stubHelmClient) RenderManifests(context.Context, *spec.ResolvedSpec, po
return s.renderResult, cleanup, nil
}

func (s *stubHelmClient) RenderOffline(context.Context, *spec.ResolvedSpec, postrenderer.PostRenderer, []extras.RawFile) (*render.RenderResult, func(), error) {
panic("unexpected RenderOffline call")
}

func (s *stubHelmClient) DeleteRelease(context.Context, string, string, bool) error {
panic("unexpected DeleteRelease call")
}
Expand Down
73 changes: 4 additions & 69 deletions internal/cmd/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"deployah.dev/deployah/internal/cmd/cmdopts"
"deployah.dev/deployah/internal/drift"
"deployah.dev/deployah/internal/extras"
"deployah.dev/deployah/internal/k8s"
"deployah.dev/deployah/internal/session"
"deployah.dev/deployah/internal/spec"

Expand All @@ -44,7 +43,6 @@ var outputFormats = []string{outputFormatText, outputFormatJSON}
type Options struct {
Environment string `nabat:"environment"`
Drift bool `nabat:"drift"`
Offline bool `nabat:"offline"`
ShowSecrets bool `nabat:"show-secrets"`
Raw bool `nabat:"raw"`
YAML bool `nabat:"yaml"`
Expand All @@ -58,8 +56,7 @@ func Register(app *nabat.App) {
nabat.WithDescription("Inspect changes for an environment"),
nabat.WithLongDescription("Render the chart for an environment and compare it with the last successful Helm release. With --drift, also compare the rendered manifests with live cluster state. Plan is read-only and never applies anything."),
nabat.WithArg("environment", "", nabat.WithRequired(), nabat.WithUsage("Environment to plan for"), nabat.WithPrompt("Environment", "", nabat.WithHint("e.g. prod, staging"))),
nabat.WithFlag("drift", false, nabat.WithUsage("Detect drift between the rendered manifests and the live cluster state (requires cluster access; not compatible with --offline)")),
nabat.WithFlag("offline", false, nabat.WithUsage("Render and validate the chart without contacting the cluster")),
nabat.WithFlag("drift", false, nabat.WithUsage("Detect drift between the rendered manifests and the live cluster state")),
nabat.WithFlag("show-secrets", false, nabat.WithUsage("Reveal masked secret values in text output (requires an interactive terminal; refused with --output json)")),
nabat.WithFlag("raw", false, nabat.WithUsage("Show raw Kubernetes field paths instead of the compact Deployah vocabulary")),
nabat.WithFlag("yaml", false, nabat.WithUsage("Show changed fields as YAML blocks instead of a single line")),
Expand All @@ -70,9 +67,6 @@ func Register(app *nabat.App) {
# Inspect changes for production
deployah plan production

# Render and validate the chart without touching the cluster
deployah plan production --offline

# Machine-readable output for CI
deployah plan production --output json

Expand All @@ -99,12 +93,6 @@ func validateOptions(c *nabat.Context) error {
if opts.ShowSecrets && !c.IsInteractive() {
return errors.New("--show-secrets requires an interactive terminal")
}
if opts.Offline && opts.OutputFormat == outputFormatJSON {
return errors.New("--offline has no diff to report as JSON; drop --output json")
}
if opts.Drift && opts.Offline {
return errors.New("--drift requires cluster access; it cannot be used with --offline")
}
return nil
}

Expand Down Expand Up @@ -143,65 +131,12 @@ func runPlan(c *nabat.Context) error {
return fmt.Errorf("resolution failed: %w", err)
}

if opts.Offline {
return runOffline(c, sess, platform, manifest, opts, resolvedSpec)
}
return runOnline(c, sess, platform, manifest, opts, resolvedSpec)
}

// runOffline renders the chart without contacting the cluster and prints a
// resource count instead of a diff: there is no reachable release history to
// compare against.
func runOffline(c *nabat.Context, sess *session.Session, platform *spec.PlatformConfig, manifest *spec.Spec, opts *Options, resolvedSpec *spec.ResolvedSpec) error {
cluster, err := sess.Target(c, opts.Environment)
if err != nil {
return fmt.Errorf("target cluster: %w", err)
}
helmClient, err := cluster.Helm()
if err != nil {
return fmt.Errorf("helm client: %w", err)
}

// Offline mode never contacts the cluster, so any self-signed TLS cert
// is generated fresh (nil client) rather than fetched/reused -- a
// deliberate offline generation, not cmdopts.MaterializeSelfSignedTLS's
// fail-closed path for an online command that couldn't build a client.
if resolvedSpec != nil {
if tlsErr := k8s.MaterializeSelfSignedTLS(c, nil, "", resolvedSpec); tlsErr != nil {
return fmt.Errorf("materialize self-signed TLS: %w", tlsErr)
}
}

bundle, err := extras.LoadFromSpec(sess.Workspace().SpecPath(), manifest, platform, opts.Environment, cluster.Namespace(), nil)
if err != nil {
return fmt.Errorf("load extras: %w", err)
}
postRenderer := bundle.PostRendererFor()

result, cleanup, err := helmClient.RenderOffline(c, resolvedSpec, postRenderer, bundle.CRDs)
if cleanup != nil {
defer cleanup()
}
if err != nil {
return fmt.Errorf("render manifests: %w", err)
}

count, err := planengine.CountResources(result.Manifest)
if err != nil {
return fmt.Errorf("count rendered resources: %w", err)
}

c.Println(fmt.Sprintf("Rendered %d resources for environment '%s' (no cluster comparison).", count, opts.Environment))
for _, d := range bundle.CRDDocs {
c.Println(fmt.Sprintf(" %s/%s", d.Kind, d.Name))
}
c.Println("validation: OK")
return nil
return executePlan(c, sess, platform, manifest, opts, resolvedSpec)
}

// runOnline renders the chart, diffs it against the last successful
// executePlan renders the chart, diffs it against the last successful
// release, and displays the resulting plan.
func runOnline(c *nabat.Context, sess *session.Session, platform *spec.PlatformConfig, manifest *spec.Spec, opts *Options, resolvedSpec *spec.ResolvedSpec) error {
func executePlan(c *nabat.Context, sess *session.Session, platform *spec.PlatformConfig, manifest *spec.Spec, opts *Options, resolvedSpec *spec.ResolvedSpec) error {
cluster, err := sess.Target(c, opts.Environment)
if err != nil {
return fmt.Errorf("target cluster: %w", err)
Expand Down
Loading
Loading