Configuration and State Management
The CLI manages configuration through two distinct persistence layers: a primary cluster definition file and a kubectl-style contexts store. The primary configuration, loaded from ~/.config/dap-cli/config.json, defines the target cluster’s network endpoints and identity realm. This file is typically bootstrapped via dap config init and is immutable during normal operation. User contexts, which bind specific clusters and users to an active session, are managed separately in ~/.config/dap-cli/contexts.json. This separation ensures that non-secret state (cluster URLs, user metadata, context bindings) is persisted consistently across invocations, while sensitive credentials are isolated in a separate, restricted store.
Cluster Configuration Loading
Section titled “Cluster Configuration Loading”The core cluster configuration is defined by the ClusterConfig dataclass and loaded via the load_config function in src/dap/config/loader.py. By default, the CLI reads from ~/.config/dap-cli/config.json 1. This file contains the portal URL, orchestrator URL, Keycloak realm UUID, and an optional CA bundle. The load_config function parses this JSON file and raises a DapError if the file is missing, malformed, or missing required keys like name or portal_url.
The dap config init command is responsible for creating this file. It accepts a portal URL and optionally an orchestrator URL and realm UUID 2. If the orchestrator URL is omitted, it is derived by substituting ‘dap-portal’ with ‘dap-orchestrator’ in the portal URL. If the realm UUID is omitted, the CLI attempts to auto-discover it from the portal; if discovery fails, it prompts the user for manual input. The resulting JSON is written to the target path with 0o644 permissions.
Credential Resolution
Section titled “Credential Resolution”Credentials are resolved dynamically at runtime via the get_credential function, which implements a resolver chain 1. The function first checks for the requested key in the environment variables (e.g., DAP_ADMIN_PASSWORD). If not found, it checks for an age-encrypted bundle if both DAP_CREDS_AGE_FILE and DAP_AGE_IDENTITY environment variables are set and the files exist. It executes the age CLI tool to decrypt the bundle and parses the output for the matching key. If neither source provides the credential, a DapError is raised with a hint to the operator.
Secrets are also managed via the CredentialStore class, which persists tokens and client secrets to a separate location with 0600 permissions 3. The dap config set-credentials command allows users to update user metadata (non-secret) in the contexts store while simultaneously storing secrets in the CredentialStore. If a client secret is not provided via CLI flags, the command prompts for it interactively.
Contexts Store and State Management
Section titled “Contexts Store and State Management”The contexts store, defined in src/dap/rest/config.py, provides a kubectl-style interface for managing clusters, users, and contexts 4. It is persisted to ~/.config/dap-cli/contexts.json. This store contains three main lists: clusters, users, and contexts, along with a current_context pointer.
- Clusters: Defined by
Clusterdataclass, containing server URL, TLS verification settings, and optional CA bundle or proxy URL. - Users: Defined by
Userdataclass, containing non-secret metadata likeclient_id,tenant_id, andgrant_type. Secrets are not stored here. - Contexts: Defined by
Contextdataclass, binding aportal_clusterandportal_userto anorchestrator_clusterandorchestrator_user, along with anoutcome(neo, dpc, ai) and optionalnamespace.
The Config class provides methods to load, save, and mutate this store. It supports upsert operations for clusters, users, and contexts by name. The resolve_binding function uses the active context (or a specified context name) to determine the correct cluster and user for a given component (portal or orchestrator). If the context is missing or references non-existent clusters/users, it raises a DapError.
CLI Configuration Commands
Section titled “CLI Configuration Commands”The dap config group provides several subcommands for managing the contexts store, registered in src/dap/rest/config_cmds.py:
set-cluster: Creates or updates a cluster entry 3.set-credentials: Creates or updates a user entry and stores secrets in theCredentialStore.set-context: Creates or updates a context, optionally setting it as the current context using the--useflag.use-context: Switches the current context to the specified name.current-context: Prints the name of the current context.rename-context: Renames an existing context.get-clusters,get-users,get-contexts: Lists configured entries, marking the current context with an asterisk.delete-cluster,delete-user,delete-context: Removes entries by name.view: Prints the entire contexts store with secrets redacted.validate: Checks for consistency, ensuring referenced clusters and users exist and the current context is valid.
"""Cluster + credential configuration loading.
The CLI reads its config from ``~/.config/dap-cli/config.json`` by
default. The file documents which cluster to talk to (portal URL,
orchestrator URL, Keycloak realm UUID). ``dap config init`` writes
this file.
All credentials are loaded via the resolver chain:
1. Environment variable (``DAP_ADMIN_PASSWORD``, ``DAP_API_CLIENT_SECRET``, ...).
2. An age-encrypted bundle if ``DAP_CREDS_AGE_FILE`` and
``DAP_AGE_IDENTITY`` are both set in the environment.
If none of the resolver steps yields the requested key, the helper
raises :class:`dap.errors.DapError` with an actionable hint that tells
the operator which environment variable to set.
"""
from __future__ import annotations
import json
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
from ..errors import DapError
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "dap-cli" / "config.json"
@dataclass(frozen=True)
class ClusterConfig:
"""One cluster's reachability + identity info.
Attributes:
name: Short identifier (used as a directory key for state files).
portal_url: ``https://dap-portal.example.com`` -- admin UI + OIDC.
orchestrator_url: ``https://dap-orchestrator.example.com`` -- REST API.
realm: Keycloak realm UUID for the customer org.
"""``dap config`` subcommand surface."""
from __future__ import annotations
import json
from pathlib import Path
import click
from ..discovery import derive_orchestrator_url, discover_realm
from ..errors import DapError
from .loader import DEFAULT_CONFIG_PATH, load_config
@click.group()
def config() -> None:
"""Manage cluster configuration."""
@config.command("init")
@click.option(
"--portal",
default=None,
help="Portal base URL, e.g. https://dap-portal.example.com. If omitted, prompted interactively.",
)
@click.option(
"--name",
default="default",
show_default=True,
help="Short cluster identifier (used as directory key for state files).",
)
@click.option(
"--orchestrator",
default=None,
help="Orchestrator base URL. If omitted, derived by substituting "
"'dap-portal' -> 'dap-orchestrator' in the portal URL.",
)
@click.option("--realm", default=None, help="Keycloak realm UUID. If omitted, auto-discovered from the portal.")
@click.option("--ca-bundle", default=None, help="Optional PEM CA file for self-signed deployments.")
@click.option("--path", default=str(DEFAULT_CONFIG_PATH), show_default=True)
"""kubectl-style ``dap config`` subcommands for the contexts store.
Registered onto the existing ``config`` group so the legacy ``config init`` /
``config show`` keep working alongside ``set-cluster`` / ``set-context`` / etc.
Secrets (token, client-secret) go to the 0600 credential store; everything else
to the non-secret contexts store.
"""
from __future__ import annotations
import click
from ..errors import DapError
from .config import Cluster, Context, User, load_config, save_config
from .credentials import CredentialStore
def _redact(value: str | None) -> str:
if not value:
return ""
return ("*" * max(0, len(value) - 4)) + value[-4:]
def register(config_group: click.Group) -> None:
@config_group.command("set-cluster")
@click.argument("name")
@click.option("--server", required=True)
@click.option("--insecure-skip-tls-verify", "insecure", is_flag=True)
@click.option("--certificate-authority", "ca_bundle", default=None)
@click.option("--proxy-url", default=None)
def set_cluster(name, server, insecure, ca_bundle, proxy_url):
"""Create or update a cluster (server + TLS)."""
cfg = load_config()
cfg.upsert_cluster(
Cluster(
name=name, server=server, insecure_skip_tls_verify=insecure, ca_bundle=ca_bundle, proxy_url=proxy_url
)
)
save_config(cfg)
click.echo(f'Cluster "{name}" set.')
"""kubectl-style configuration for the generic ``/rest/v1`` command layer.
The model mirrors the Go CLI it replaces: a set of *clusters* (servers + TLS),
*users* (non-secret credential metadata), and *contexts* (bindings of a portal
cluster/user and an orchestrator cluster/user plus the selected outcome). A
context can split the portal and orchestrator bindings because a DAP install
exposes two API servers that share one OIDC issuer.
Non-secret data lives in ``~/.config/dap-cli/contexts.json``. Secret material
(bearer tokens, client secrets) never lands here -- it is held separately at
mode 0600 by :mod:`dap.rest.credentials`. The legacy task-oriented commands
keep their own ``config.json`` untouched; this store is additive.
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from ..errors import DapError
DEFAULT_STORE_PATH = Path.home() / ".config" / "dap-cli" / "contexts.json"
SCHEMA_VERSION = 1
OUTCOMES = ("neo", "dpc", "ai")
@dataclass(frozen=True)
class Cluster:
name: str
server: str
insecure_skip_tls_verify: bool = False
ca_bundle: str | None = None
proxy_url: str | None = None
@dataclass(frozen=True)
class User:
"""Non-secret credential metadata. Secrets live in the credential store."""