Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/on-pr-kind-smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
FIXTURE=tests/fixtures/config-smoke
test -d "$FIXTURE"
test -f "$FIXTURE/upbound.yaml"
CONFIGURATION=config-smoke
CONFIGURATION=hops-ops-config-smoke

./target/debug/hops-cli config install --path "$FIXTURE" --cluster-provider kind --docker-provider docker

Expand Down
34 changes: 22 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ spec:
From that project root:

```bash
hops local up
hops local gitops cluster ./.gitops/local/cluster
hops local gitops cluster ./.gitops/local/cluster.yaml
hops local gitops environment ./.gitops/local/environment.yaml --name main
```

Expand All @@ -131,17 +130,27 @@ From another checkout of the same project:
hops local gitops environment ./.gitops/local/environment.yaml --name feature-auth
```

`up` validates the Cluster before starting or reusing it. `gitops cluster`
watches shared `.gitops/local/cluster` manifests. `environment` validates the
Environment against that Cluster, renders each deploy's `.gitops/promote`
chart, applies the resulting local Applications to the runtime namespace, and
watches `.gitops/local/environment.yaml` plus the referenced
`.gitops/promote` and `.gitops/local` charts. Each application's
`gitops cluster` validates the Cluster, starts or resumes it, bootstraps the
local control plane, and watches the declared shared manifests.
`environment` validates the Environment against that Cluster, turns each
deploy's `.gitops/local` chart (or explicit `deploys[].chart`) into a local
Application, applies it to the runtime namespace, and watches
`.gitops/local/environment.yaml` plus those chart roots. Each application's
`.gitops/local` chart owns its editable local workload; `.gitops/deploy` is a
separate cloud workload chart selected by promotion outside local mode.
The runtime name, namespace, checkout path, and Cluster binding are local state;
they are not committed to the Cluster definition.

Use the same commands for teardown:

```bash
hops local gitops environment --name feature-auth --down
hops local gitops cluster ./.gitops/local/cluster.yaml --down
```

Deleting a watched Environment definition also purges and unregisters its
runtime Environment.

An existing kind Cluster with a different exact `mountRoot` fails with an
explicit reset/recreate instruction and is never silently deleted. A legacy
directory of pre-rendered Application YAMLs is still accepted by `environment`
Expand Down Expand Up @@ -633,7 +642,7 @@ Notes:
- `config install --repo ...` now prompts in interactive terminals to choose between cloning/building from source or applying a published package version. Published-version prompts suggest the latest discovered tag by default and still accept arbitrary tags such as `pr-<gitsha>`.
- Non-interactive `config install --repo ...` keeps the previous default behavior and builds from source.
- `config install --repo ... --version ...` skips clone/build and applies the remote package directly.
- `config uninstall --repo ...` derives the configuration name as `<org>-<repo>`.
- `config uninstall --repo ...` uses the cached `_output/*.uppkg` package identity when available. Without cached artifacts, it assumes the published OCI package is `ghcr.io/<org>/<repo>`.

## Commands

Expand Down Expand Up @@ -676,7 +685,7 @@ Notes:
- `config install --repo <org/repo> --version <tag>`
- Remote-package mode that can target any connected cluster
- Skips clone/build and applies `Configuration` with package `ghcr.io/<org>/<repo>:<tag>`
- Uses configuration name `<org>-<repo>` (for example `hops-ops-aws-auto-eks-cluster`)
- Uses configuration name `<org>-<package>` (for example `hops-ops-aws-auto-eks-cluster`)
- Does not support `--reload`
- Supports `--skip-dependency-resolution`
- `config uninstall --name <configuration-name>`
Expand All @@ -685,8 +694,9 @@ Notes:
- Prunes orphaned `Configuration`/`Function`/`Provider` packages and revisions no longer present in lock
- Prunes orphaned `ImageConfig` rewrites for removed render functions
- `config uninstall --repo <org/repo>`
- Targets configuration name `<org>-<repo>`
- If cached repo exists at `~/.hops/local/repo-cache/<org>/<repo>`, derives source hints from it for additional package pruning
- Uses package identity from cached `_output/*.uppkg` artifacts when available, so the repository and packaged OCI names may differ
- Without cached artifacts, assumes the published OCI package is `ghcr.io/<org>/<repo>`
- If cached repo exists at `~/.hops/local/repo-cache/<org>/<repo>`, also derives source hints from it for additional package pruning
- `config uninstall --path <PATH>`
- Derives target configuration names from `<PATH>/_output/*.uppkg` image tags
- Also derives package sources from those artifacts and prunes matching package resources (including Functions) if they remain
Expand Down
7 changes: 7 additions & 0 deletions bootstrap/registry/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,20 @@ spec:
scheme: HTTPS
initialDelaySeconds: 2
periodSeconds: 5
# A laptop resuming several local control planes can briefly take
# seconds to service TLS. Do not turn CPU contention into a
# destructive registry restart loop.
timeoutSeconds: 15
failureThreshold: 6
livenessProbe:
httpGet:
path: /v2/
port: 5000
scheme: HTTPS
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 15
failureThreshold: 6
volumes:
- name: registry-data
persistentVolumeClaim:
Expand Down
6 changes: 6 additions & 0 deletions skills/claude/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ Full detail: [local-source-packages.md](references/local-source-packages.md).

- **Crossplane 2+**: Use `managementPolicies`, never `deletionPolicy` on managed resources
- **Packages**: Prefer `crossplane-contrib` packages over Upbound-hosted ones (paid-account restrictions)
- **Package object names**: Name every Crossplane package-manager object Hops creates
(`Configuration`, `Provider`, or explicit `Function`) from its OCI identity as
`<org>-<package>`—for example, `hops-ops-secret-stack`. Source, published, and
GitOps installs must target that same object. Package metadata and GitOps filenames
may use the short package name, but the installed object's `metadata.name` must not;
a short alias creates duplicate package-source conflicts in the Crossplane lock.
- **Commits**: Conventional Commits (`feat:`, `fix:`, `chore:`) with subjects under 72 chars
- **XRD projects**: Use Upbound-format projects with `upbound.yaml`, `apis/`, `functions/`, `tests/`
- **Testing**: `make render` for quick validation, `up test run tests/test-render` for unit tests, `up test run tests/e2etest-* --e2e` for E2E
14 changes: 11 additions & 3 deletions skills/claude/references/config-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,13 @@ The CLI handles cleanup automatically when switching modes:

## Configuration Naming

Configurations are named `<org>-<repo>`, e.g. `hops-ops-aws-secret-stack`.
This matches both local and published installs. Gitops package **filenames** use
Configurations are named `<org>-<package>`, e.g. `hops-ops-secret-stack`.
This matches source, published, and GitOps installs. GitOps package **filenames** use
the short package name (`psql-stack.yaml`); `metadata.name` matches the applied
Configuration.
Configuration. The package's internal metadata may also remain short, but the
installed `Configuration.metadata.name` must use the canonical OCI-derived
name. Never create a short alias beside it: Crossplane rejects duplicate package
sources in its lock.

## Uninstall

Expand All @@ -183,6 +186,11 @@ hops config uninstall --repo hops-ops/aws-auto-eks-cluster
hops config uninstall --path /path/to/project
```

`--repo` uses the package identity in cached `_output/*.uppkg` artifacts when
available. This supports source repositories whose packaged OCI name differs from
the repository name. Without cached artifacts, it assumes the published package
is `ghcr.io/<org>/<repo>`. `--path` always derives names from its build artifacts.

Uninstall waits for lock reconciliation and prunes orphaned packages (Configurations,
Functions, Providers) and ImageConfig rewrites.

Expand Down
38 changes: 21 additions & 17 deletions skills/claude/references/local-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
## Quick Start

```bash
# 1. Start local k8s + Crossplane + providers + registry
# (provider selection is user-local: ~/.hops/local/providers.json)
hops local start --cluster-provider kind --docker-provider dory --cluster-name hops
# 1. Start/resume the declared cluster and watch shared GitOps manifests
hops local gitops cluster ./.gitops/local/cluster.yaml

# 2. Install platform packages into the CP *and* pin them in cluster gitops
# 2. Add or update platform packages in .gitops/local/cluster when needed
hops config install --repo hops-ops/psql-stack --version v0.9.1 \
--gitops ./gitops/cluster --local
--gitops ./.gitops/local/cluster --local
hops config install --repo hops-ops/auth-stack --version v1.6.0 \
--gitops ./gitops/cluster --local
--gitops ./.gitops/local/cluster --local

# 3. Watch/apply cluster gitops (packages + XRs). Or pass --gitops on start.
hops local gitops cluster ./gitops/cluster
# 3. Register this checkout as an Environment
hops local gitops environment ./.gitops/local/environment.yaml --name main

# 4. Optional cloud provider auth (writes live Secrets; use --gitops for non-secret YAML)
hops local aws --profile hops
Expand All @@ -31,7 +30,13 @@ to `default` (scaffolded by `config install --gitops --local`). See
### `hops local install`
Installs Colima via Homebrew.

### `hops local start`
### `hops local gitops cluster <cluster.yaml>`

This is the normal lifecycle command. It validates the Kubernetes-shaped
Cluster definition, invokes the local start/bootstrap pipeline, and applies +
watches the definition's `spec.manifests.path`.

The underlying `hops local start` command:
- Starts the chosen backend (colima / kind / dory)
- Installs **pinned** Crossplane Helm chart (`CROSSPLANE_CHART_VERSION` in `start.rs`)
- Applies bootstrap Providers (pinned tags in `bootstrap/providers/`):
Expand All @@ -40,13 +45,9 @@ Installs Colima via Homebrew.
- Applies ProviderConfigs named `default`, local registry, DRCs
- Configures node trust for the in-cluster registry

With **`--gitops PATH`** (e.g. `./gitops/cluster`):
1. Writes the same helm/k8s bootstrap into the tree (`providers/`, `providerconfigs/`, `runtime/`)
2. Runs `hops local gitops cluster PATH` (apply + watch) so day-to-day CP state is gitops-owned

```bash
hops local start --cluster-provider kind --docker-provider dory \
--cluster-name hops --gitops ./gitops/cluster
hops local gitops cluster ./.gitops/local/cluster.yaml
hops local gitops cluster ./.gitops/local/cluster.yaml --down
```

**Version bumps:** Renovate owns these pins (`cli/renovate.json` customManagers →
Expand All @@ -62,8 +63,11 @@ hops provider install --path /path/to/provider-helm --gitops ./gitops/cluster

See [local-source-packages.md](./local-source-packages.md).

### `hops local stop` / `hops local destroy` / `hops local uninstall`
Stop, delete, or uninstall Colima respectively.
### Cluster teardown

`hops local gitops cluster <cluster.yaml> --down` stops the declared Cluster
while preserving its data. `hops local destroy` remains the explicit,
destructive cluster deletion command; `hops local uninstall` removes tooling.

### `hops local aws --profile <PROFILE>`

Expand Down
16 changes: 9 additions & 7 deletions skills/claude/references/local-workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ tree copy). You do not need to learn volume types.

```bash
# Dory app running (engine healthy). Product Dory Kubernetes is optional.
hops local up
hops local gitops cluster ./.gitops/local/cluster
hops local gitops cluster ./.gitops/local/cluster.yaml
```

Context is typically `kind-hops`. Confirm mounts:
Expand All @@ -36,14 +35,15 @@ Stock Dory k8s (`--cluster-provider dory --docker-provider dory`) is fine for pl
**cannot** hostPath-mount Mac paths into the node; delivery falls back to sync.

```bash
hops local up --cluster-provider dory --docker-provider dory
hops local gitops cluster ./.gitops/local/cluster.yaml \
--cluster-provider dory --docker-provider dory
```

## Daily loop

```bash
# Shared CP watch (if start did not use --gitops, or after Ctrl+C)
hops local gitops cluster ./.gitops/local/cluster
# Start/resume the Cluster and watch its shared control-plane manifests
hops local gitops cluster ./.gitops/local/cluster.yaml

# One Environment per checkout (namespace = --name) — watches by default
hops local gitops environment ./.gitops/local/environment.yaml --name dogfood
Expand All @@ -52,6 +52,9 @@ hops local gitops environment ./.gitops/local/environment.yaml --name dogfood
```

Watch is the default for both gitops commands. Use `--once` for a single reconcile (CI/scripts).
Use `environment --name <name> --down` to purge one Environment and
`cluster <cluster.yaml> --down` to stop the control plane while preserving its
named node volume.

## Concurrent worktrees

Expand All @@ -73,8 +76,7 @@ Each name maps to namespace `<name>`.
```bash
cd distributed/tests/e2e-ui
# Prefer kind-on-Dory for hostPath HMR (see One-time prerequisite)
hops local up
hops local gitops cluster ./.gitops/local/cluster
hops local gitops cluster ./.gitops/local/cluster.yaml
hops local gitops environment ./.gitops/local/environment.yaml --name dogfood
```

Expand Down
58 changes: 11 additions & 47 deletions src/commands/config/install.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::commands::config::configuration_name_from_package_ref;
use crate::commands::local::backend::{self, Backend, ClusterProvider, DockerProvider};
use crate::commands::local::package_install::run_watch;
use crate::commands::local::package_install::{
docker_arch, ensure_cached_repo_checkout, ensure_registry, image_config_name,
parse_docker_push_digest, parse_repo_spec, registry_pull, registry_push,
resolve_repo_install_target, rewrite_registry, rewrite_registry_with_tag,
sanitize_name_component, short_hash, split_ref, strip_registry, unique_suffix,
RepoInstallTarget, RepoSpec,
resolve_repo_install_target, rewrite_registry, rewrite_registry_with_tag, short_hash,
split_ref, strip_registry, unique_suffix, RepoInstallTarget, RepoSpec,
};
use crate::commands::local::{kubectl_apply_stdin, kubectl_command, run_cmd, run_cmd_output};
use clap::Args;
Expand Down Expand Up @@ -106,11 +106,6 @@ struct PackageMetadataName {
name: String,
}

#[derive(Debug, Deserialize)]
struct ConfigurationPackageMetadata {
metadata: PackageMetadataName,
}

#[derive(Debug, Deserialize)]
struct PackageSpec {
#[serde(rename = "package")]
Expand Down Expand Up @@ -215,11 +210,7 @@ fn apply_repo_version_spec(
}

let package_ref = format!("ghcr.io/{}/{}:{}", spec.org, spec.repo, version);
let config_name = format!(
"{}-{}",
sanitize_name_component(&spec.org),
sanitize_name_component(&spec.repo)
);
let config_name = configuration_name_from_package_ref(&package_ref);

// Delete any existing render Function so Crossplane re-resolves with the
// correct digest for this version (avoids conflicts when switching between
Expand Down Expand Up @@ -416,7 +407,7 @@ spec:
);
let mut source_to_push = img.source.clone();
let package_yaml = extract_package_yaml_from_uppkg(&img.uppkg_path, &img.source)?;
let configuration_name = configuration_name_from_package_yaml(&package_yaml, &pull_ref);
let configuration_name = configuration_name_from_package_ref(&pull_ref);
configurations.push((configuration_name, pull_ref.clone()));
let (patched_yaml, changed) =
rewrite_render_dependency_digests(&package_yaml, &render_rewrites);
Expand Down Expand Up @@ -627,21 +618,6 @@ fn is_configuration_image(image: &str) -> bool {
split_ref(image).1 == "configuration"
}

/// Prefer the package author's declared metadata.name so a source install
/// updates the same Configuration object as a published GitOps pin. Fall back
/// to the historical registry-path name for older packages without metadata.
fn configuration_name_from_package_yaml(package_yaml: &str, pull_ref: &str) -> String {
serde_yaml::Deserializer::from_str(package_yaml)
.next()
.and_then(|document| ConfigurationPackageMetadata::deserialize(document).ok())
.map(|package| sanitize_name_component(&package.metadata.name))
.filter(|name| !name.is_empty())
.unwrap_or_else(|| {
let (image_path, _) = split_ref(pull_ref);
strip_registry(image_path).replace('/', "-")
})
}

fn extract_package_yaml_from_uppkg(
uppkg_path: &Path,
configuration_image: &str,
Expand Down Expand Up @@ -1131,32 +1107,20 @@ spec:
}

#[test]
fn source_install_uses_declared_configuration_name() {
let package_yaml = r#"apiVersion: meta.pkg.crossplane.io/v1
kind: Configuration
metadata:
name: secret-stack
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: secretstores.hops.ops.com.ai
"#;
fn source_install_uses_registry_package_identity() {
assert_eq!(
configuration_name_from_package_yaml(
package_yaml,
configuration_name_from_package_ref(
"registry.crossplane-system.svc.cluster.local:5000/hops-ops/secret-stack:dev-abc"
),
"secret-stack"
"hops-ops-secret-stack"
);
}

#[test]
fn source_install_name_falls_back_to_registry_path() {
fn source_install_name_sanitizes_registry_path_components() {
assert_eq!(
configuration_name_from_package_yaml(
"apiVersion: meta.pkg.crossplane.io/v1\nkind: Configuration\n",
"registry.crossplane-system.svc.cluster.local:5000/hops-ops/secret-stack:dev-abc"
configuration_name_from_package_ref(
"registry.crossplane-system.svc.cluster.local:5000/Hops_Ops/Secret.Stack:dev-abc"
),
"hops-ops-secret-stack"
);
Expand Down
Loading
Loading