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
38 changes: 27 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,21 @@ hops config install --repo hops-ops/aws-auto-eks-cluster --version v0.11.0

### Cluster backends

`hops local` supports two backends behind the same commands:
`hops local` supports three backends behind the same commands:

- **colima** — a VM running dockerd + k3s. macOS/Linux; supports `--cpus`,
`--memory`, `--disk`, and `hops local resize`.
- **kind** — cluster nodes as docker containers on any reachable docker
daemon: Docker Desktop, colima's dockerd, [dory](https://augani.github.io/dory),
or CI runners. No VM of its own, so sizing flags don't apply (size the
docker daemon instead); requires kind >= v0.27.
daemon: Docker Desktop, colima's dockerd, or CI runners. No VM of its own,
so sizing flags don't apply (size the docker daemon instead); requires
kind >= v0.27.
- **dory** — [dory](https://augani.github.io/dory)'s built-in k3s, driven
headlessly through the `dory` CLI (`dory k8s enable/disable/status`).
Requires the Dory app running (it provides the engine and forwards
published ports to localhost) and a `dory` CLI with headless k8s support.
hops writes `~/.dory/k8s/registries.yaml` (k3s' native registry trust) and
publishes the registry NodePort at cluster create. The VM is sized in the
Dory app, so hops sizing flags don't apply.

Select with the global `--backend` flag:

Expand All @@ -236,22 +243,31 @@ persisted choice > existing cluster detection (colima wins) > platform
default (macOS: colima, otherwise kind).

Unless `--context` is given, kubectl commands automatically use the backend's
kubeconfig context (`colima` or `kind-hops`), regardless of your
current-context.
kubeconfig context (`colima`, `kind-hops`, or `dory`), regardless of your
current-context. For dory, hops also prepends `~/.kube/dory-config` (where
dory keeps its kubeconfig) to `KUBECONFIG` for its own kubectl/helm calls.

#### Using dory

[dory](https://augani.github.io/dory) exposes a real docker socket, so run the
kind backend against it:
With a `dory` CLI that supports `dory k8s enable` (headless Kubernetes),
use the native backend:

```bash
hops local start --backend dory
```

For `hops provider install` / `hops config install` builds, point your docker
CLI at dory's engine (`export DOCKER_HOST=unix://$HOME/.dory/engine.sock`) so
image builds/pushes land on the daemon that reaches the registry.

Without the headless CLI, dory still exposes a real docker socket, so the
kind backend works against it:

```bash
docker context use dory # or: export DOCKER_HOST=unix://$HOME/.dory/dory.sock
hops local start --backend kind
```

Don't use dory's built-in Kubernetes for hops — it publishes only the API
port, so the local package registry would be unreachable from the host.

### Local provider setup and auth

`hops local aws`, `hops local github`, and `hops local zitadel` install the provider package and bootstrap auth into a local control plane. The exception is `--refresh`, which updates credentials only.
Expand Down
73 changes: 57 additions & 16 deletions src/commands/config/install.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use crate::commands::local::backend::{self, wire_local_registry};
use crate::commands::local::backend::{self, Backend};
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, resolve_repo_install_target, rewrite_registry,
rewrite_registry_with_tag, sanitize_name_component, short_hash, split_ref, strip_registry,
unique_suffix, RepoInstallTarget, RepoSpec, REGISTRY_PULL, REGISTRY_PUSH,
};
use crate::commands::local::{
kubectl_apply_stdin, kubectl_command, run_cmd, run_cmd_output, HOPS_KUBE_CONTEXT_ENV,
};
use crate::commands::local::{kubectl_apply_stdin, kubectl_command, run_cmd, run_cmd_output};
use clap::Args;
use flate2::read::GzDecoder;
use serde::Deserialize;
Expand Down Expand Up @@ -43,6 +41,10 @@ pub struct ConfigArgs {
#[arg(long)]
pub context: Option<String>,

/// Local cluster backend whose node should be wired for local package pulls.
#[arg(long, value_enum)]
pub backend: Option<Backend>,

/// Watch the project directory for changes and re-run install automatically
#[arg(long, conflicts_with = "repo")]
pub watch: bool,
Expand Down Expand Up @@ -108,17 +110,22 @@ struct PackageResource {
}

pub fn run(args: &ConfigArgs) -> Result<(), Box<dyn Error>> {
if let Some(ctx) = &args.context {
std::env::set_var(HOPS_KUBE_CONTEXT_ENV, ctx);
}
let backend = backend::activate(args.backend, args.context.as_deref());

match (args.repo.as_deref(), args.version.as_deref()) {
(Some(repo), Some(version)) => {
apply_repo_version(repo, version, args.skip_dependency_resolution)
}
(Some(repo), None) => run_repo_install(repo, args.skip_dependency_resolution),
(Some(repo), None) => run_repo_install(
repo,
args.skip_dependency_resolution,
backend,
args.backend,
args.context.as_deref(),
),
(None, _) => {
let path = args.path.as_deref().unwrap_or(".");
prepare_local_registry(backend, args.backend, args.context.as_deref())?;
run_local_path(path, args.skip_dependency_resolution)?;

if args.watch {
Expand All @@ -134,21 +141,49 @@ pub fn run(args: &ConfigArgs) -> Result<(), Box<dyn Error>> {
}
}

fn run_repo_install(repo: &str, skip_dependency_resolution: bool) -> Result<(), Box<dyn Error>> {
fn run_repo_install(
repo: &str,
skip_dependency_resolution: bool,
backend: Backend,
backend_flag: Option<Backend>,
context: Option<&str>,
) -> Result<(), Box<dyn Error>> {
let spec = parse_repo_spec(repo)?;
match resolve_repo_install_target(&spec)? {
RepoInstallTarget::SourceBuild => run_repo_clone(&spec, skip_dependency_resolution),
RepoInstallTarget::SourceBuild => run_repo_clone(
&spec,
skip_dependency_resolution,
backend,
backend_flag,
context,
),
RepoInstallTarget::PublishedVersion(version) => {
apply_repo_version_spec(&spec, &version, skip_dependency_resolution)
}
}
}

fn run_repo_clone(spec: &RepoSpec, skip_dependency_resolution: bool) -> Result<(), Box<dyn Error>> {
let cache_path = ensure_cached_repo_checkout(&spec)?;
fn run_repo_clone(
spec: &RepoSpec,
skip_dependency_resolution: bool,
backend: Backend,
backend_flag: Option<Backend>,
context: Option<&str>,
) -> Result<(), Box<dyn Error>> {
let cache_path = ensure_cached_repo_checkout(spec)?;
prepare_local_registry(backend, backend_flag, context)?;
run_local_path(&cache_path.to_string_lossy(), skip_dependency_resolution)
}

fn prepare_local_registry(
backend: Backend,
backend_flag: Option<Backend>,
context: Option<&str>,
) -> Result<(), Box<dyn Error>> {
ensure_registry()?;
backend::wire_local_registry_for_target(backend, backend_flag, context)
}

fn apply_repo_version_spec(
spec: &RepoSpec,
version: &str,
Expand Down Expand Up @@ -216,9 +251,6 @@ fn run_local_path(path: &str, skip_dependency_resolution: bool) -> Result<(), Bo
return Err(format!("{} is not a directory", path).into());
}

ensure_registry()?;
wire_local_registry(backend::resolve(None))?;

// Build the Crossplane package
log::info!("Building Crossplane package in {}...", path);
let status = Command::new("up")
Expand All @@ -237,7 +269,7 @@ fn run_local_path(path: &str, skip_dependency_resolution: bool) -> Result<(), Bo
let packages: Vec<_> = fs::read_dir(&output_dir)
.map_err(|e| format!("Failed to read {}: {}", output_dir.display(), e))?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "uppkg"))
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "uppkg"))
.collect();

if packages.is_empty() {
Expand Down Expand Up @@ -1038,6 +1070,15 @@ spec:
assert!(!without_skip.contains("skipDependencyResolution: true"));
}

#[test]
fn local_registry_wiring_skips_foreign_context_without_backend_flag() {
assert!(!backend::should_wire_local_registry(
None,
Some("kind-hops"),
Backend::Colima
));
}

#[test]
fn package_source_strips_tag_and_digest() {
assert_eq!(
Expand Down
Loading