Registry tokens: name and namespace the generated pull secret

76419e8 · gmorell · 2026-10-01 20:35

9 files +126 -7
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/live/registry_token_live/index.ex
+55 −7
@@ -24,6 +24,8 @@ defmodule GitGudWeb.RegistryTokenLive.Index do
24 24 |> GitGudWeb.RepoLive.Header.assign_chrome(repo)
25 25 |> assign(:registry_host, Application.get_env(:git_gud, :registry_host))
26 26 |> assign(:new_token, nil)
27 + |> assign(:secret_name, "")
28 + |> assign(:secret_namespace, "")
27 29 |> assign(:all_scopes, Packages.RegistryToken.scopes())
28 30 |> load()
29 31 |> assign(:page_title, "Registry tokens — #{owner}/#{name}")}
@@ -63,6 +65,15 @@ defmodule GitGudWeb.RegistryTokenLive.Index do
63 65 end
64 66 end
65 67
68 + # The pull-secret preview's name and namespace, as typed. Blank means the
69 + # ghost default: `<repo>-pull`, and no namespace.
70 + def handle_event("secret_meta", params, socket) do
71 + {:noreply,
72 + socket
73 + |> assign(:secret_name, String.trim(params["secret_name"] || ""))
74 + |> assign(:secret_namespace, String.trim(params["secret_namespace"] || ""))}
75 + end
76 +
66 77 def handle_event("delete_token", %{"id" => id}, socket) do
67 78 if socket.assigns.can_admin? do
68 79 :ok = Packages.delete_registry_token(socket.assigns.repo, String.to_integer(id))
@@ -82,10 +93,18 @@ defmodule GitGudWeb.RegistryTokenLive.Index do
82 93
83 94 defp image_ref(assigns), do: "#{assigns.registry_host}/#{assigns.handle}/#{assigns.repo.name}"
84 95
96 + defp default_secret_name(repo), do: "#{repo.name}-pull"
97 +
98 + defp secret_name(%{secret_name: "", repo: repo}), do: default_secret_name(repo)
99 + defp secret_name(%{secret_name: name}), do: name
100 +
85 101 # Ready-to-apply kubernetes.io/dockerconfigjson secret for pulling this
86 − # repo's images with the freshly-minted token.
102 + # repo's images with the freshly-minted token, under the name and in the
103 + # namespace typed above it — the ones the Deployment's imagePullSecrets
104 + # will name. Without a namespace it lands wherever kubectl defaults to,
105 + # which is rarely where the Deployment is, so the YAML says so.
87 106 defp pull_secret_yaml(assigns) do
88 − %{registry_host: host, repo: repo, new_token: %{name: user, secret: pass}} = assigns
107 + %{registry_host: host, new_token: %{name: user, secret: pass}} = assigns
89 108 auth = Base.encode64("#{user}:#{pass}")
90 109
91 110 dockercfg =
@@ -93,12 +112,18 @@ defmodule GitGudWeb.RegistryTokenLive.Index do
93 112 "auths" => %{host => %{"username" => user, "password" => pass, "auth" => auth}}
94 113 })
95 114
115 + namespace =
116 + case assigns.secret_namespace do
117 + "" -> " # no namespace: apply with `kubectl -n <the Deployment's namespace>`\n"
118 + ns -> " namespace: #{ns}\n"
119 + end
120 +
96 121 """
97 122 apiVersion: v1
98 123 kind: Secret
99 124 metadata:
100 − name: #{repo.name}-pull
101 − type: kubernetes.io/dockerconfigjson
125 + name: #{secret_name(assigns)}
126 + #{namespace}type: kubernetes.io/dockerconfigjson
102 127 data:
103 128 .dockerconfigjson: #{Base.encode64(dockercfg)}
104 129 """
@@ -146,13 +171,36 @@ defmodule GitGudWeb.RegistryTokenLive.Index do
146 171 podman login {@registry_host} -u {@new_token.name} -p {@new_token.secret}
147 172 </span>
148 173 </p>
149 − <details :if={@registry_host} class="text-xs">
174 + <details :if={@registry_host} class="text-xs" open>
150 175 <summary class="cursor-pointer opacity-80">Kubernetes imagePullSecret</summary>
176 + <form phx-change="secret_meta" class="flex flex-wrap gap-2 mt-2">
177 + <label class="form-control">
178 + <span class="opacity-70">Secret name</span>
179 + <input
180 + name="secret_name"
181 + value={@secret_name}
182 + placeholder={default_secret_name(@repo)}
183 + phx-debounce="200"
184 + class="input input-xs input-bordered font-mono"
185 + />
186 + </label>
187 + <label class="form-control">
188 + <span class="opacity-70">Namespace</span>
189 + <input
190 + name="secret_namespace"
191 + value={@secret_namespace}
192 + placeholder="the Deployment's namespace"
193 + phx-debounce="200"
194 + class="input input-xs input-bordered font-mono"
195 + />
196 + </label>
197 + </form>
151 198 <pre class="bg-black text-green-300 p-2 rounded overflow-auto select-all mt-1">{pull_secret_yaml(assigns)}</pre>
152 199 <p class="opacity-70 mt-1">
153 200 Apply it, then set <code>imagePullSecrets</code>
154 − to <code>{@repo.name}-pull</code>
155 − for image <code>{image_ref(assigns)}:thetag</code>.
201 + to <code>{secret_name(assigns)}</code>
202 + for image <code>{image_ref(assigns)}:thetag</code>
203 + — in the same namespace: a pod only sees pull secrets in its own.
156 204 </p>
157 205 </details>
158 206 </div>
added test/git_gud_web/live/registry_token_live_test.exs
+71 −0
@@ -0,0 +1,71 @@
1 +defmodule GitGudWeb.RegistryTokenLiveTest do
2 + @moduledoc """
3 + The registry-token page's pull-secret preview: it has to come out under
4 + the name and namespace the Deployment will look in, or the cluster pulls
5 + anonymously and gets 401 with the token never used.
6 + """
7 + use GitGudWeb.ConnCase, async: false
8 +
9 + import GitGud.ForgeFixtures
10 + import Phoenix.LiveViewTest
11 +
12 + alias GitGud.Repositories.Storage
13 +
14 + @moduletag :git_required
15 +
16 + setup do
17 + if System.find_executable("git") == nil do
18 + {:skip, "git not on PATH"}
19 + else
20 + previous = Application.get_env(:git_gud, :registry_host)
21 + Application.put_env(:git_gud, :registry_host, "ggr.example.com")
22 +
23 + on_exit(fn ->
24 + if previous,
25 + do: Application.put_env(:git_gud, :registry_host, previous),
26 + else: Application.delete_env(:git_gud, :registry_host)
27 + end)
28 +
29 + {owner, repo} = repository_fixture()
30 + repo = GitGud.Repo.preload(repo, [:owner, :organization])
31 + path = "/r/#{Storage.repo_handle(repo)}/#{repo.name}/settings/registry-tokens"
32 + {:ok, owner: owner, repo: repo, path: path}
33 + end
34 + end
35 +
36 + defp create_token(view) do
37 + view
38 + |> form("form[phx-submit=create_token]", %{"name" => "argo-pull", "scopes" => ["pull"]})
39 + |> render_submit()
40 + end
41 +
42 + test "defaults to <repo>-pull with no namespace, and says to pass -n",
43 + %{conn: conn, owner: owner, repo: repo, path: path} do
44 + {:ok, view, _html} = conn |> log_in_user(owner) |> live(path)
45 + html = create_token(view)
46 +
47 + assert html =~ "name: #{repo.name}-pull"
48 + refute html =~ ~r/\n\s*namespace: /
49 + assert html =~ "apply with `kubectl -n"
50 + assert html =~ ~s(placeholder="#{repo.name}-pull")
51 + end
52 +
53 + test "uses the name and namespace typed in",
54 + %{conn: conn, owner: owner, path: path} do
55 + {:ok, view, _html} = conn |> log_in_user(owner) |> live(path)
56 + create_token(view)
57 +
58 + html =
59 + view
60 + |> form("form[phx-change=secret_meta]", %{
61 + "secret_name" => "ggr-pull",
62 + "secret_namespace" => "ws-tower"
63 + })
64 + |> render_change()
65 +
66 + assert html =~ "name: ggr-pull"
67 + assert html =~ ~r/\n\s*namespace: ws-tower\n/
68 + refute html =~ "apply with `kubectl -n"
69 + assert html =~ "to <code>ggr-pull</code>"
70 + end
71 +end

Parents: 713c997