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
9 changes: 5 additions & 4 deletions .github/workflows/on-pr-kind-smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,21 @@ jobs:
FIXTURE=tests/fixtures/config-smoke
test -d "$FIXTURE"
test -f "$FIXTURE/upbound.yaml"
CONFIGURATION=config-smoke

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

for i in $(seq 1 90); do
healthy="$(kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke \
healthy="$(kubectl --context kind-hops get configuration.pkg.crossplane.io "$CONFIGURATION" \
-o jsonpath='{.status.conditions[?(@.type=="Healthy")].status}' 2>/dev/null || true)"
installed="$(kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke \
installed="$(kubectl --context kind-hops get configuration.pkg.crossplane.io "$CONFIGURATION" \
-o jsonpath='{.status.conditions[?(@.type=="Installed")].status}' 2>/dev/null || true)"
if [ "$healthy" = "True" ] && [ "$installed" = "True" ]; then
echo "Configuration hops-ops-config-smoke Healthy after ${i} attempts"
echo "Configuration $CONFIGURATION Healthy after ${i} attempts"
break
fi
if [ "$i" -eq 90 ]; then
kubectl --context kind-hops get configuration.pkg.crossplane.io hops-ops-config-smoke -o yaml || true
kubectl --context kind-hops get configuration.pkg.crossplane.io "$CONFIGURATION" -o yaml || true
kubectl --context kind-hops get configurationrevisions.pkg.crossplane.io -o wide || true
kubectl --context kind-hops get functions.pkg.crossplane.io -o wide || true
exit 1
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ rusoto_sts = "0.48.0"
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
uuid = { version = "1.17.0", features = ["v4"] }
notify = "8.2.0"
libc = "0.2"

[features]
vendored = ["openssl-sys/vendored"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ Notes:
- Source-build mode intended for a local control plane because it depends on the local registry flow
- Runs `up project build` in `PATH` (defaults to current directory)
- Loads generated `.uppkg` artifacts from `<PATH>/_output`
- Pushes package images to the registry exposed at `127.0.0.1:30500` (IPv4; avoids Docker's `localhost` → `[::1]` path)
- Pushes package images to the backend-specific registry endpoint selected by the CLI: `127.0.0.1:30500` when Docker runs locally, or `{dory-k8s-ip}:30500` for Dory
- Applies Crossplane `Configuration` resources pointing at `registry.crossplane-system.svc.cluster.local:5000/...`
- Supports `--skip-dependency-resolution`
- `config install --repo <org/repo> [--reload]`
Expand Down
38 changes: 1 addition & 37 deletions skills/claude/references/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Overview

`hops secrets` manages repo-level secrets using SOPS for encryption and syncs
to AWS Secrets Manager, GitHub repository secrets, or HashiCorp Vault (KV).
to AWS Secrets Manager or GitHub repository secrets.

## Setup

Expand All @@ -20,11 +20,9 @@ secrets/ # Plaintext (gitignored)
aws/
github/
_shared/
vault/ # → hops secrets sync vault (KV paths)
secrets-encrypted/ # SOPS-encrypted (committed)
aws/
github/
vault/
```

### Configuration (`.hops.yaml`)
Expand All @@ -46,18 +44,6 @@ secrets:
repos:
- repo-a
- repo-b
vault:
path: vault
address: http://127.0.0.1:8200 # or $VAULT_ADDR
mount: secret # KV mount
version: v2
path_prefix: "" # optional prefix on every remote path
token_env: VAULT_TOKEN
kube: # port-forward when address is down
enabled: true
namespace: vault
service: vault
local_port: 8200
```

## Encrypt / Decrypt
Expand Down Expand Up @@ -108,25 +94,3 @@ hops secrets sync github
- `.env` files → one secret per `KEY=value` entry
- Shared secrets fan out to all repos in `shared_secrets.repos`
- Repo-specific values override shared values

## Sync to HashiCorp Vault (KV)

```bash
export VAULT_TOKEN=root # local SecretStack dev Vault; never commit
hops secrets sync vault
hops secrets sync vault --secret-path secrets/vault/e2e-ui/dogfood -y
hops secrets sync vault --port-forward # force kubectl tunnel to in-cluster Vault
```

### Vault naming rules (same roll-up as AWS)

| Source | Vault KV path (mount `secret`) |
|--------|--------------------------------|
| `secrets/vault/e2e-ui/dogfood/oidc.json` | `e2e-ui/dogfood/oidc` (JSON object → properties) |
| `secrets/vault/e2e-ui/dogfood/human-passwords/{alice,bob}` | `e2e-ui/dogfood/human-passwords` |
| `secrets/vault/auth/zitadel-masterkey/masterkey` | `auth/zitadel-masterkey` property `masterkey` |

- Paths match ExternalSecret `remoteRef.key` (no `secret/data/` prefix)
- Writer token is separate from ESO’s read-only kubernetes auth role
- Unchanged remote maps are skipped (compare-before-write)
- Local Vault chart is often in-memory: re-run sync after `vault-0` restarts; keep SOPS plaintext as the durable copy
44 changes: 40 additions & 4 deletions src/commands/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,19 @@ pub(crate) fn wait_for_kubernetes() -> Result<(), Box<dyn Error>> {
log::info!("Waiting for Kubernetes API...");
// ~10 minutes — nested-virt apiserver can stay overloaded after package install.
for _ in 0..120 {
let result = run_cmd_output("kubectl", &["get", "--raw", "/readyz"]);
if result.is_ok() {
if kubectl_command(&["get", "--raw", "/readyz"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
{
return Ok(());
}
// Fall back to a cheap list if /readyz is denied on some setups.
if run_cmd_output("kubectl", &["get", "ns", "default"]).is_ok() {
if kubectl_command(&["get", "ns", "default"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
{
return Ok(());
}
std::thread::sleep(std::time::Duration::from_secs(5));
Expand Down Expand Up @@ -357,19 +364,48 @@ pub fn kubectl_apply_stdin(yaml: &str) -> Result<(), Box<dyn Error>> {
if !stderr.trim().is_empty() {
eprint!("{stderr}");
}
if !is_transient_kubectl_apply_error(&stderr) {
return Err(format!("kubectl apply exited with {last_err}").into());
}
if attempt == ATTEMPTS {
break;
}
log::warn!(
"kubectl apply failed (attempt {}/{}, {}); waiting for API...",
attempt,
ATTEMPTS,
output.status
);
let _ = wait_for_kubernetes();
wait_for_kubernetes().map_err(|e| {
format!("kubectl apply exited with {last_err}; API recovery failed: {e}")
})?;
std::thread::sleep(std::time::Duration::from_secs(10));
}

Err(format!("kubectl apply exited with {last_err} after retries").into())
}

fn is_transient_kubectl_apply_error(stderr: &str) -> bool {
let lower = stderr.to_ascii_lowercase();
[
"unable to connect to the server",
"connection refused",
"connection reset",
"connection timed out",
"i/o timeout",
"tls handshake timeout",
"context deadline exceeded",
"dial tcp",
"no route to host",
"service unavailable",
"gateway timeout",
"server is currently unable to handle the request",
"unexpected eof",
]
.iter()
.any(|needle| lower.contains(needle))
}

/// Apply a JSON merge patch with `kubectl patch --type merge`.
/// Automatically injects `--context` when configured.
pub fn kubectl_patch_merge(
Expand Down
Loading
Loading