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
29 changes: 0 additions & 29 deletions .ai/context.md

This file was deleted.

110 changes: 88 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
# hops-cli

`hops-cli` is a Rust CLI for running a local Crossplane development environment on Colima.
`hops-cli` is a Rust CLI for Crossplane development and XR lifecycle workflows.

## Overview

This tool manages a local Kubernetes stack and package workflow for Crossplane:
This tool supports three related workflows:

- Installs and manages Colima
- Starts a local k8s cluster with Crossplane installed via Helm
- Installs the Kubernetes and Helm Crossplane providers
- Deploys an in-cluster OCI registry (`crossplane-system/registry`)
- Builds and publishes Crossplane configuration packages from an XRD project
- Local cluster setup on Colima
- Configuration package install/uninstall against the connected cluster
- XR observe/manage/adopt/orphan workflows for existing infrastructure

For local development, it can also:

- Install and manage Colima
- Start a local k8s cluster with Crossplane installed via Helm
- Install the Kubernetes and Helm Crossplane providers
- Deploy an in-cluster OCI registry (`crossplane-system/registry`)
- Build and publish Crossplane configuration packages from an XRD project

## Installation

Expand Down Expand Up @@ -70,6 +76,8 @@ cargo build --features vendored
hops --help
hops local --help
hops config --help
hops validate --help
hops xr --help
```

From source without installing:
Expand All @@ -78,6 +86,8 @@ From source without installing:
cargo run -- --help
cargo run -- local --help
cargo run -- config --help
cargo run -- validate --help
cargo run -- xr --help
```

## Quick Start
Expand All @@ -96,22 +106,34 @@ cargo run -- local aws --profile <aws-profile>
cargo run -- local github --owner <org-or-user>

# 5) Build and load a Crossplane configuration package from an XRD project
cargo run -- local config --path /path/to/project
cargo run -- config install --path /path/to/project

# 6) Build from a GitHub repo (cached clone + build + push to local registry)
cargo run -- local config --repo hops-ops/helm-certmanager
cargo run -- config install --repo hops-ops/helm-certmanager

# 7) Force reload from source (deletes existing ConfigurationRevision(s) first)
cargo run -- local config --repo hops-ops/helm-certmanager --reload
cargo run -- config install --repo hops-ops/helm-certmanager --reload

# 8) Apply a pinned remote package version directly (no clone/build)
cargo run -- local config --repo hops-ops/helm-certmanager --version v0.1.0
cargo run -- config install --repo hops-ops/helm-certmanager --version v0.1.0

# 9) Remove a configuration and prune orphaned package dependencies
cargo run -- local unconfig --repo hops-ops/helm-certmanager
cargo run -- config uninstall --repo hops-ops/helm-certmanager

# 10) Generate apis/*/configuration.yaml from upbound.yaml for validation
cargo run -- config generate --path /path/to/project
cargo run -- validate generate-configuration --path /path/to/project

# 11) Observe an existing XR into a manifest
cargo run -- xr observe --kind AutoEKSCluster --name pat-local --namespace default --aws-region us-east-2

# 12) Render adoption patches for managed resources under an existing XR
cargo run -- xr adopt --kind AutoEKSCluster --name pat-local --namespace default

# 13) Convert an observed/adopted XR into a managed manifest
cargo run -- xr manage --kind AutoEKSCluster --name pat-local --namespace default

# 14) Render patches that remove Delete from management policies
cargo run -- xr orphan --kind AutoEKSCluster --name pat-local --namespace default
```

## Commands
Expand All @@ -132,31 +154,32 @@ cargo run -- config generate --path /path/to/project
- Runs `colima delete --force`.
- `local uninstall`
- Prompts for confirmation, then runs `brew uninstall colima`.
- `local config [--path <PATH>] [--reload]`
- `config install [--path <PATH>] [--reload]`
- Targets the currently connected Kubernetes cluster
- Runs `up project build` in `PATH` (defaults to current directory)
- Loads generated `.uppkg` artifacts from `<PATH>/_output`
- Pushes package images to local registry (`localhost:30500`)
- Pushes package images to the registry exposed at `localhost:30500`
- Applies Crossplane `Configuration` resources pointing at `registry.crossplane-system.svc.cluster.local:5000/...`
- `local config --repo <org/repo> [--reload]`
- `config install --repo <org/repo> [--reload]`
- Uses local repo cache at `~/.hops/local/repo-cache/<org>/<repo>`
- Clones on first use, then fetches/pulls on subsequent runs
- Runs the same build/load/push/apply flow as `--path`
- `--reload`
- Forces source-based config (`--path` or `--repo` without `--version`) to delete existing `ConfigurationRevision` resources and matching `Function`/`FunctionRevision` package resources from the same sources, then re-apply the `Configuration`
- Forces source-based config install (`--path` or `--repo` without `--version`) to delete existing `ConfigurationRevision` resources and matching `Function`/`FunctionRevision` package resources from the same sources, then re-apply the `Configuration`
- Useful when re-running a config and you want Crossplane to re-create the current revision from source
- `local config --repo <org/repo> --version <tag>`
- `config install --repo <org/repo> --version <tag>`
- Skips clone/build and applies `Configuration` with package `ghcr.io/<org>/<repo>:<tag>`
- Uses configuration name `<org>-<repo>` (for example `hops-ops-helm-certmanager`)
- Does not support `--reload`
- `local unconfig --name <configuration-name>`
- `config uninstall --name <configuration-name>`
- Deletes the target `Configuration`
- Waits for package lock reconciliation
- Prunes orphaned `Configuration`/`Function`/`Provider` packages and revisions no longer present in lock
- Prunes orphaned `ImageConfig` rewrites for removed render functions
- `local unconfig --repo <org/repo>`
- `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
- `local unconfig --path <PATH>`
- `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
- `local aws [--profile <AWS_PROFILE>]`
Expand All @@ -177,10 +200,53 @@ cargo run -- config generate --path /path/to/project
- Applies a Secret (`github-creds`) and GitHub `ProviderConfig` (`default`) in namespace `default`
- `--refresh` updates only the Secret credentials and skips Provider/ProviderConfig apply
- Supports overrides via `--namespace`, `--secret-name`, `--provider-config-name`, `--provider-name`, and `--provider-package`
- `config generate [--path <PATH>] [--api-path <APIS_PATH>]`
- `validate generate-configuration [--path <PATH>] [--api-path <APIS_PATH>]`
- Reads `<PATH>/upbound.yaml` and writes `<APIS_PATH>/configuration.yaml`
- Auto-detects `--api-path` via `apis/*/definition.yaml` when omitted
- Ensures `apis/**/configuration.yaml` is present in `<PATH>/.gitignore` (unless `--no-gitignore-update`)
- `xr observe --kind <KIND> --name <NAME> --namespace <NAMESPACE> --aws-region <REGION>`
- Generates an observe-only XR manifest for an existing resource
- Loads the live XR from the cluster when present
- Enriches the manifest with live AWS discovery for supported XR kinds such as `AutoEKSCluster` and `Network`
- Supports `--output` and `--apply`
- `xr adopt --kind <KIND> --name <NAME> --namespace <NAMESPACE>`
- Lists managed resources that belong to the XR and renders metadata patches needed for adoption
- For `AutoEKSCluster`, uses the composite-specific label `hops.ops.com.ai/autoekscluster=<name>`
- Only emits patches for resources whose external name is missing or blank and can be resolved for that kind
- Supports `--apply`, `--output`, and `--recursive`
- `xr manage --kind <KIND> --name <NAME> --namespace <NAMESPACE>`
- Generates the final managed XR manifest from an observed or adopted XR already in the cluster
- Supports `--output` and `--apply`
- `xr orphan --kind <KIND> --name <NAME> --namespace <NAMESPACE>`
- Renders managed-resource patches that remove `Delete` from management policies
- Supports `--apply` and `--output`

## XR workflow

Typical reclaim flow:

```bash
# 1) Observe the existing resource into an XR manifest
hops xr observe --kind AutoEKSCluster --name pat-local --namespace default --aws-region us-east-2 --output observed.yaml

# 2) Apply the observe XR if desired
kubectl apply -f observed.yaml

# 3) Render and apply adoption patches for the next set of managed resources
hops xr adopt --kind AutoEKSCluster --name pat-local --namespace default --apply

# 4) Repeat adopt until no more patches are needed, or use --recursive
hops xr adopt --kind AutoEKSCluster --name pat-local --namespace default --recursive --apply

# 5) Convert the XR into a managed manifest
hops xr manage --kind AutoEKSCluster --name pat-local --namespace default --output managed.yaml
```

Notes:

- `xr adopt` only patches resources it can identify for the selected XR kind.
- A blank `crossplane.io/external-name` is treated as missing.
- `AutoEKSCluster` adoption currently resolves identities for supported managed kinds such as IAM attachments and KMS keys.

## Logging

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{
use crate::commands::local::{
kubectl_apply_stdin, repo_cache_path, run_cmd, run_cmd_output, sync_registry_hosts_entry,
};
use clap::Args;
Expand Down
12 changes: 8 additions & 4 deletions src/commands/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod generate_configuration;
mod install;
mod uninstall;

use clap::{Args, Subcommand};
use std::error::Error;
Expand All @@ -11,12 +12,15 @@ pub struct ConfigArgs {

#[derive(Subcommand, Debug)]
pub enum ConfigCommands {
/// Generate api metadata configuration.yaml from upbound.yaml
Generate(generate_configuration::GenerateArgs),
/// Build and load a Crossplane configuration into the local cluster
Install(install::ConfigArgs),
/// Remove a Crossplane configuration and prune orphaned package dependencies
Uninstall(uninstall::UnconfigArgs),
}

pub fn run(args: &ConfigArgs) -> Result<(), Box<dyn Error>> {
match &args.command {
ConfigCommands::Generate(generate_args) => generate_configuration::run(generate_args),
ConfigCommands::Install(install_args) => install::run(install_args),
ConfigCommands::Uninstall(uninstall_args) => uninstall::run(uninstall_args),
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{repo_cache_path, run_cmd, run_cmd_output};
use crate::commands::local::{repo_cache_path, run_cmd, run_cmd_output};
use clap::Args;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
Expand Down
8 changes: 0 additions & 8 deletions src/commands/local/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
mod aws;
mod config;
mod destroy;
mod github;
mod install;
mod reset;
mod start;
mod stop;
mod unconfig;
mod uninstall;

use clap::{Args, Subcommand};
Expand Down Expand Up @@ -42,10 +40,6 @@ pub enum LocalCommands {
Destroy,
/// Uninstall Colima
Uninstall,
/// Build and load a Crossplane configuration into the local cluster
Config(config::ConfigArgs),
/// Remove a Crossplane configuration and prune orphaned package dependencies
Unconfig(unconfig::UnconfigArgs),
}

pub fn run(args: &LocalArgs) -> Result<(), Box<dyn Error>> {
Expand All @@ -58,8 +52,6 @@ pub fn run(args: &LocalArgs) -> Result<(), Box<dyn Error>> {
LocalCommands::Stop => stop::run(),
LocalCommands::Destroy => destroy::run(),
LocalCommands::Uninstall => uninstall::run(),
LocalCommands::Config(config_args) => config::run(config_args),
LocalCommands::Unconfig(unconfig_args) => unconfig::run(unconfig_args),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod config;
pub mod local;
pub mod validate;
pub mod xr;
24 changes: 24 additions & 0 deletions src/commands/validate/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
mod generate_configuration;

use clap::{Args, Subcommand};
use std::error::Error;

#[derive(Args, Debug)]
pub struct ValidateArgs {
#[command(subcommand)]
pub command: ValidateCommands,
}

#[derive(Subcommand, Debug)]
pub enum ValidateCommands {
/// Generate api metadata configuration.yaml from upbound.yaml
GenerateConfiguration(generate_configuration::GenerateArgs),
}

pub fn run(args: &ValidateArgs) -> Result<(), Box<dyn Error>> {
match &args.command {
ValidateCommands::GenerateConfiguration(generate_args) => {
generate_configuration::run(generate_args)
}
}
}
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ struct Args {
enum Commands {
/// Manage the local development environment
Local(commands::local::LocalArgs),
/// Manage Crossplane configuration project helpers
/// Manage Crossplane configuration packages in the connected cluster
Config(commands::config::ConfigArgs),
/// Manage validation helpers for Crossplane projects
Validate(commands::validate::ValidateArgs),
/// Manage live XR observe/manage/adopt workflows
Xr(commands::xr::XrArgs),
}
Expand All @@ -34,6 +36,9 @@ fn main() -> Result<(), Box<dyn Error>> {
Some(Commands::Config(config_args)) => {
commands::config::run(config_args)?;
}
Some(Commands::Validate(validate_args)) => {
commands::validate::run(validate_args)?;
}
Some(Commands::Xr(xr_args)) => {
commands::xr::run(xr_args)?;
}
Expand Down
Loading