@meistrari/mise-en-place 2.18.3 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,20 +26,34 @@ if [ -z "$FILE_NAME" ]; then
26
26
  exit 1
27
27
  fi
28
28
 
29
+ # Maybe in the future we can make this script more flexible and allow the user
30
+ # to specify the environment as an argument or environment variable.
31
+ # For now, we will hardcode it to development.
29
32
  ENV=development
30
33
 
31
- SECRET_PATH="$APP_NAME/$ENV/$WORKLOAD_PATH"
34
+ # Reads a secret from the kv mount and appends its values to $FILE_NAME.
35
+ # Aborts the script if the secret cannot be read.
36
+ append_secrets() {
37
+ local secret_path="$1"
38
+ local secret_json
32
39
 
33
- if ! SECRET_JSON="$(vault kv get -mount=kv -format=json "$SECRET_PATH" 2>&1)"; then
34
- echo "Failed to read kv/$SECRET_PATH from $VAULT_ADDR:" >&2
35
- echo "$SECRET_JSON" >&2
36
- echo "" >&2
37
- echo "Inspect it in the browser: $VAULT_ADDR/ui/vault/secrets/kv/kv/list/${APP_NAME}/" >&2
38
- exit 1
39
- fi
40
+ if ! secret_json="$(vault kv get -mount=kv -format=json "$secret_path" 2>&1)"; then
41
+ echo "Failed to read kv/$secret_path from $VAULT_ADDR:" >&2
42
+ echo "$secret_json" >&2
43
+ echo "" >&2
44
+ echo "Inspect it in the browser: $VAULT_ADDR/ui/vault/secrets/kv/kv/list/$(dirname "$secret_path")/" >&2
45
+ exit 1
46
+ fi
47
+
48
+ echo "$secret_json" | jq -r '.data.data | to_entries[] | "\(.key)=\(.value | @sh)"' >> "$FILE_NAME"
49
+ }
40
50
 
41
51
  echo "# Values from VaultSecrets" > "$FILE_NAME"
42
52
  echo "# Edits to this file may be overwritten" >> "$FILE_NAME"
43
53
 
44
- echo "$SECRET_JSON" \
45
- | jq -r '.data.data | to_entries[] | "\(.key)=\(.value | @sh)"' >> "$FILE_NAME"
54
+ # Shared values come first so app-specific ones override them on collision.
55
+ if [ "$ENV" == "development" ]; then
56
+ append_secrets "shared/$ENV"
57
+ fi
58
+
59
+ append_secrets "$APP_NAME/$ENV/$WORKLOAD_PATH"
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Login to Harbor (internal registry). Opens the UI so you can copy your CLI secret."
3
+ #USAGE flag "-r --registry <registry>" default="harbor.internal.tela.tools" help="Harbor host to authenticate against." env="HARBOR_REGISTRY"
4
+ #USAGE flag "-u --username <username>" default="" help="Harbor OIDC username. Prompted for when omitted." env="HARBOR_USERNAME"
5
+ #USAGE flag "-f --force" default=#false help="Re-authenticate even if the stored credential still works."
6
+ #USAGE flag "--browser" negate="--no-browser" default=#true help="Open the Harbor UI so the CLI secret can be copied."
7
+ #MISE quiet=true
8
+ #MISE interactive=true
9
+
10
+ set -euo pipefail
11
+
12
+ REGISTRY="${usage_registry:-registry.internal.tela.tools}"
13
+ USERNAME="${usage_username:-}"
14
+
15
+ # Harbor runs authMode: oidc_auth, so the Google password is not a valid docker
16
+ # credential. Harbor mints a per-user CLI secret instead, shown in the UI. This
17
+ # is that copy step, wrapped.
18
+
19
+ die() { echo "error: $*" >&2; exit 1; }
20
+
21
+ # `externalURL` is the VPN-only internal gateway, so an unreachable host almost
22
+ # always means NetBird is down rather than Harbor being broken. Say so, because
23
+ # the raw curl failure sends people to the wrong place.
24
+ check_reachable() {
25
+ curl -fsS --max-time 8 -o /dev/null "https://${REGISTRY}/api/v2.0/systeminfo" 2>/dev/null
26
+ }
27
+
28
+ # Presence of a credential is not the same as a working one: CLI secrets get
29
+ # rotated and OIDC sessions lapse. Ask Harbor rather than reading the keychain.
30
+ #
31
+ # Checked against /v2/ (the registry API) rather than /api/v2.0/users/current,
32
+ # because a robot credential answers 412 on the latter — it has no user profile —
33
+ # which would report a perfectly good pull credential as broken. /v2/ is also
34
+ # the endpoint that actually governs `docker pull`, so it tests what we care about.
35
+ check_credential() {
36
+ local auth
37
+ auth=$(docker-credential-"$(docker_cred_store)" get <<<"${REGISTRY}" 2>/dev/null) || return 1
38
+ local user secret
39
+ user=$(jq -r '.Username // empty' <<<"$auth")
40
+ secret=$(jq -r '.Secret // empty' <<<"$auth")
41
+ [ -n "$user" ] && [ -n "$secret" ] || return 1
42
+ curl -fsS --max-time 8 -o /dev/null -u "${user}:${secret}" \
43
+ "https://${REGISTRY}/v2/" 2>/dev/null
44
+ }
45
+
46
+ docker_cred_store() {
47
+ jq -r '.credsStore // "osxkeychain"' ~/.docker/config.json 2>/dev/null || echo osxkeychain
48
+ }
49
+
50
+ open_browser() {
51
+ local url="https://${REGISTRY}"
52
+ if command -v open >/dev/null 2>&1; then
53
+ open "$url"
54
+ elif command -v xdg-open >/dev/null 2>&1; then
55
+ xdg-open "$url"
56
+ else
57
+ echo "Open this manually: $url"
58
+ fi
59
+ }
60
+
61
+ command -v docker >/dev/null 2>&1 || die "docker is not installed"
62
+ command -v jq >/dev/null 2>&1 || die "jq is not installed"
63
+
64
+ check_reachable || die "cannot reach ${REGISTRY}. It is only routable over the NetBird VPN — connect and retry."
65
+
66
+ if [ "${usage_force:-false}" != "true" ] && check_credential; then
67
+ echo "Already logged in to ${REGISTRY}."
68
+ # A working credential is usually what people want, so staying logged in is
69
+ # the default. Offer the way out anyway: CLI secrets are per-user, and
70
+ # switching accounts otherwise means remembering --force.
71
+ if [ -t 0 ]; then
72
+ read -r -p "Log out and re-authenticate? [y/N] " answer || true
73
+ else
74
+ answer=""
75
+ fi
76
+ case "${answer:-}" in
77
+ [yY]|[yY][eE][sS])
78
+ # Drop the stored credential first, so a failed re-login does not
79
+ # leave the old account silently in place.
80
+ docker logout "$REGISTRY" >/dev/null
81
+ ;;
82
+ *)
83
+ exit 0
84
+ ;;
85
+ esac
86
+ fi
87
+
88
+ if [ -z "$USERNAME" ]; then
89
+ # Harbor identities come from Google (Dex), so only a work address is a
90
+ # plausible default. A personal git email would just be a wrong prefill.
91
+ default_user="$(git config --get user.email 2>/dev/null || true)"
92
+ case "$default_user" in
93
+ *@meistrari.com|*@tela.com) ;;
94
+ *) default_user="" ;;
95
+ esac
96
+ read -r -p "Harbor username${default_user:+ [$default_user]}: " USERNAME || true
97
+ USERNAME="${USERNAME:-$default_user}"
98
+ [ -n "$USERNAME" ] || die "a username is required"
99
+ fi
100
+
101
+ if [ "${usage_browser:-true}" = "true" ]; then
102
+ cat <<EOF
103
+
104
+ In the Harbor UI:
105
+ 1. Sign in through Google (Dex) if prompted
106
+ 2. Open the avatar menu (top right) -> User Profile
107
+ 3. Copy the CLI secret
108
+
109
+ EOF
110
+ # Pause before launching, so the steps above can be read rather than being
111
+ # buried the instant the browser takes focus.
112
+ read -r -p "Press ENTER to open ${REGISTRY} in your browser... " _ || true
113
+ open_browser
114
+ fi
115
+
116
+ # -s so the secret never reaches the terminal or shell history.
117
+ read -r -s -p "Paste your Harbor CLI secret: " CLI_SECRET || true
118
+ echo
119
+
120
+ [ -n "$CLI_SECRET" ] || die "no CLI secret entered"
121
+
122
+ printf '%s' "$CLI_SECRET" | docker login "$REGISTRY" --username "$USERNAME" --password-stdin
123
+
124
+ echo "Logged in to ${REGISTRY} as ${USERNAME}."
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
1
  {
2
- "name": "@meistrari/mise-en-place",
3
- "version": "2.18.3",
4
- "description": "",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/meistrari/mise-en-place.git"
8
- },
9
- "scripts": {
10
- "build": "unbuild"
11
- },
12
- "bin": {
13
- "mise-en-place": "dist/cli/cli.mjs"
14
- },
15
- "exports": {
16
- "./eslint": {
17
- "types": "./dist/eslint.d.ts",
18
- "import": "./dist/eslint.mjs"
2
+ "name": "@meistrari/mise-en-place",
3
+ "version": "2.19.0",
4
+ "description": "",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/meistrari/mise-en-place.git"
8
+ },
9
+ "scripts": {
10
+ "build": "unbuild"
11
+ },
12
+ "bin": {
13
+ "mise-en-place": "dist/cli/cli.mjs"
14
+ },
15
+ "exports": {
16
+ "./eslint": {
17
+ "types": "./dist/eslint.d.ts",
18
+ "import": "./dist/eslint.mjs"
19
+ }
20
+ },
21
+ "files": [
22
+ ".editorconfig",
23
+ "Makefile",
24
+ "dist",
25
+ "scripts",
26
+ "tsconfig.base.json",
27
+ "mise"
28
+ ],
29
+ "keywords": [],
30
+ "author": "",
31
+ "license": "ISC",
32
+ "type": "module",
33
+ "bugs": {
34
+ "url": "https://github.com/meistrari/mise-en-place/issues"
35
+ },
36
+ "homepage": "https://github.com/meistrari/mise-en-place#readme",
37
+ "dependencies": {
38
+ "@antfu/eslint-config": "6.7.3",
39
+ "dedent": "1.7.1",
40
+ "eslint": "9.39.4",
41
+ "eslint-plugin-diff": "2.1.7",
42
+ "eslint-plugin-drizzle": "0.2.3",
43
+ "glob": "13.0.6",
44
+ "jsonc-parser": "3.3.1",
45
+ "smol-toml": "^1.6.1",
46
+ "yaml": "2.8.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "24.12.0",
50
+ "unbuild": "3.6.1"
19
51
  }
20
- },
21
- "files": [
22
- ".editorconfig",
23
- "Makefile",
24
- "dist",
25
- "scripts",
26
- "tsconfig.base.json",
27
- "mise"
28
- ],
29
- "keywords": [],
30
- "author": "",
31
- "license": "ISC",
32
- "type": "module",
33
- "bugs": {
34
- "url": "https://github.com/meistrari/mise-en-place/issues"
35
- },
36
- "homepage": "https://github.com/meistrari/mise-en-place#readme",
37
- "dependencies": {
38
- "@antfu/eslint-config": "6.7.3",
39
- "dedent": "1.7.1",
40
- "eslint": "9.39.4",
41
- "eslint-plugin-diff": "2.1.7",
42
- "eslint-plugin-drizzle": "0.2.3",
43
- "glob": "13.0.6",
44
- "jsonc-parser": "3.3.1",
45
- "smol-toml": "^1.6.1",
46
- "yaml": "2.8.2"
47
- },
48
- "devDependencies": {
49
- "@types/node": "24.12.0",
50
- "unbuild": "3.6.1"
51
- }
52
52
  }