diff --git a/README.md b/README.md index b7e3df4..0205c25 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Metis produces fully configured recovery SD cards for any node in the lab (RPi 4 - `metis-sentinel` stays slim for the DaemonSet that samples node facts - Class overlays: define `boot_overlay`/`root_overlay` on a class to merge static files into boot/root at burn time (e.g., cloud-init/netplan drop-ins, GPU driver configs). Per-node config still injects hostname/IP/k3s/SSH/Longhorn. - Linux loop-mount helper (losetup/mount) exists for automation; wiring into CLI burn is next. Windows writer/GUI stub forthcoming. -- Vault: Metis can read per-node secrets from `secret/data/nodes/` using VAULT_ADDR plus either VAULT_TOKEN or AppRole (VAULT_ROLE_ID/VAULT_SECRET_ID). Expected fields: ssh_password, k3s_token, cloud_init, extra map. +- Vault: Metis can read per-node secrets from `kv/data/atlas/nodes/` using VAULT_ADDR plus either VAULT_TOKEN or AppRole (VAULT_ROLE_ID/VAULT_SECRET_ID). Expected fields: ssh_password, k3s_token, cloud_init, extra map. - Sentinel: `metis-sentinel` collects host facts and can either print them, write local history, or push them into the Metis service. The intended deployment shape is a DaemonSet on cluster nodes plus an Ariadne-triggered Metis watch that recomputes recommended class targets and drift history. - Facts aggregation: `metis facts --inventory inv.yaml --snapshots ./snapshots` reads sentinel snapshot JSON files and prints per-class drift summary (kernels, containerd, k3s, package samples). Use exported ConfigMaps or `METIS_SENTINEL_OUT` history as input. - `metis config --inventory inv.yaml --node titan-13` prints the merged node config (hostname/IP/k3s labels/taints/Longhorn UUIDs and optional USB scratch metadata). diff --git a/pkg/plan/inject_extra_test.go b/pkg/plan/inject_extra_test.go index 3f68f4b..3c8e280 100644 --- a/pkg/plan/inject_extra_test.go +++ b/pkg/plan/inject_extra_test.go @@ -15,7 +15,7 @@ import ( func TestFilesAndInjectWithSecretsAndOverlays(t *testing.T) { vault := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/v1/secret/data/nodes/titan-15" { + if r.URL.Path != "/v1/kv/data/atlas/nodes/titan-15" { http.NotFound(w, r) return } diff --git a/pkg/secrets/coverage_more_test.go b/pkg/secrets/coverage_more_test.go index d8fc806..ccfd35e 100644 --- a/pkg/secrets/coverage_more_test.go +++ b/pkg/secrets/coverage_more_test.go @@ -14,11 +14,11 @@ func TestClientLoginAndFetchBranches(t *testing.T) { switch { case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/auth/approle/login"): _ = json.NewEncoder(w).Encode(map[string]any{"auth": map[string]any{"client_token": "token"}}) - case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/secret/data/nodes/missing"): + case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/kv/data/atlas/nodes/missing"): w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/secret/data/nodes/error"): + case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/kv/data/atlas/nodes/error"): http.Error(w, "boom", http.StatusInternalServerError) - case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/secret/data/nodes/node1"): + case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/kv/data/atlas/nodes/node1"): _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ "data": map[string]any{"k3s_token": "abc", "cloud_init": "ci"}, @@ -101,7 +101,7 @@ func TestClientFetchAdditionalErrorBranches(t *testing.T) { } badJSON := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/secret/data/nodes/node1") { + if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/kv/data/atlas/nodes/node1") { _, _ = w.Write([]byte(`{bad-json`)) return } diff --git a/pkg/secrets/vault.go b/pkg/secrets/vault.go index 5653681..74a0712 100644 --- a/pkg/secrets/vault.go +++ b/pkg/secrets/vault.go @@ -13,7 +13,7 @@ import ( ) // NodeSecrets holds per-node secret material to inject at burn time. -// These should live in Vault at secret/data/nodes/. +// These should live in Vault at kv/data/atlas/nodes/. type NodeSecrets struct { SSHPassword string `json:"ssh_password,omitempty"` SSHPasswordHash string `json:"ssh_password_hash,omitempty"` @@ -88,12 +88,12 @@ func (c *Client) LoginIfNeeded(ctx context.Context) error { // FetchNode loads per-node secret material because burn-time injection needs // a single read path that can fall back to empty secrets when Vault has no row. -// FetchNode pulls secret/data/nodes/. +// FetchNode pulls kv/data/atlas/nodes/. func (c *Client) FetchNode(ctx context.Context, hostname string) (*NodeSecrets, error) { if err := c.LoginIfNeeded(ctx); err != nil { return nil, err } - url := fmt.Sprintf("%s/v1/secret/data/nodes/%s", strings.TrimSuffix(c.Addr, "/"), hostname) + url := fmt.Sprintf("%s/v1/kv/data/atlas/nodes/%s", strings.TrimSuffix(c.Addr, "/"), hostname) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err diff --git a/pkg/secrets/vault_test.go b/pkg/secrets/vault_test.go index deb2cf1..839ff80 100644 --- a/pkg/secrets/vault_test.go +++ b/pkg/secrets/vault_test.go @@ -11,7 +11,7 @@ import ( func TestFetchNodeReturnsData(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v1/secret/data/nodes/n1": + case "/v1/kv/data/atlas/nodes/n1": w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ @@ -52,7 +52,7 @@ func TestApproRoleLogin(t *testing.T) { "client_token": "newtoken", }, }) - case "/v1/secret/data/nodes/n1": + case "/v1/kv/data/atlas/nodes/n1": if r.Header.Get("X-Vault-Token") != "newtoken" { t.Fatalf("missing token after approle login") } @@ -103,7 +103,7 @@ func TestFetchNodeAndLoginErrorBranches(t *testing.T) { switch r.URL.Path { case "/v1/auth/approle/login": http.Error(w, "denied", http.StatusForbidden) - case "/v1/secret/data/nodes/missing": + case "/v1/kv/data/atlas/nodes/missing": http.Error(w, "down", http.StatusInternalServerError) default: http.NotFound(w, r) diff --git a/pkg/service/remote_helpers.go b/pkg/service/remote_helpers.go index a1f2ddd..37f260e 100644 --- a/pkg/service/remote_helpers.go +++ b/pkg/service/remote_helpers.go @@ -445,7 +445,7 @@ export METIS_SSH_KEY_HECATE_DB="{{ .Data.data.hecate_db_pub }}" } nodeHostname = strings.TrimSpace(nodeHostname) if nodeHostname != "" { - secretPath := fmt.Sprintf("secret/data/nodes/%s", nodeHostname) + secretPath := fmt.Sprintf("kv/data/atlas/nodes/%s", nodeHostname) annotations["vault.hashicorp.com/agent-inject-secret-metis-node-secrets-env.sh"] = secretPath annotations["vault.hashicorp.com/agent-inject-template-metis-node-secrets-env.sh"] = `{{ with secret "` + secretPath + `" }} export METIS_NODE_SSH_PASSWORD="{{ .Data.data.ssh_password }}" diff --git a/pkg/service/remote_helpers_test.go b/pkg/service/remote_helpers_test.go index b45c5cc..17acf83 100644 --- a/pkg/service/remote_helpers_test.go +++ b/pkg/service/remote_helpers_test.go @@ -270,7 +270,7 @@ func TestRemoteWorkspaceAndHostTmpPathsPreferUsbScratchRoots(t *testing.T) { t.Fatalf("expected desired metadata env, got %#v", buildEnv) } metadataAnnotations := buildSpec["metadata"].(map[string]any)["annotations"].(map[string]string) - if metadataAnnotations["vault.hashicorp.com/agent-inject-secret-metis-node-secrets-env.sh"] != "secret/data/nodes/titan-10" { + if metadataAnnotations["vault.hashicorp.com/agent-inject-secret-metis-node-secrets-env.sh"] != "kv/data/atlas/nodes/titan-10" { t.Fatalf("unexpected node secret annotation: %#v", metadataAnnotations) } if !strings.Contains(metadataAnnotations["vault.hashicorp.com/agent-inject-template-metis-node-secrets-env.sh"], "METIS_NODE_ROOT_PASSWORD") {