Files
at-container-registry/pkg/appview/handlers/install.go
T
Evan JarrettandClaude Opus 5 9d8bd513da appview: render the install scripts from config instead of shipping ATCR's
seamark.dev's /install and /settings/devices told users to pipe
seamark.dev/static/install.sh into bash. That file was the unmodified ATCR
script: it announced itself as the "ATCR Credential Helper Installer",
installed docker-credential-atcr, and finished by telling the user to configure
credHelpers for atcr.io, the wrong registry for that deployment. Anyone
following the documented setup ended up pointed at another service. The
templates hardcoded docker-credential-atcr, "atcr" and ~/.atcr/device.json
alongside a correctly themed {{ .RegistryURL }}.

The scripts are now rendered from config by a handler, rather than forked per
brand. A theme overlay was the alternative and was worse: it needed a full copy
of both install.sh and install.ps1 per brand, four scripts to keep in sync, and
the operator asked for these values to come from config.

credential_helper.name is the single knob. Docker resolves a credHelpers value
x by exec'ing docker-credential-x, so the credHelpers value, the binary suffix
and the config directory are genuinely one word, not three that can drift. It
is validated against a strict pattern because it is interpolated into a shell
script.

install.sh renders byte-identical to the deleted static file under the atcr
default, so existing installs are unaffected. install.ps1 differs by one line,
where a stale usage comment named a path the script is not served at.

Two behaviour changes worth noting: these two URLs drop from a one-year
Cache-Control to five minutes, since the body now depends on deployment config;
and credential_helper.tangled_repo becomes a real overridable default. It was
previously assigned over unconditionally and read by nothing, while the shipped
script used a different URL form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
2026-09-02 21:38:10 -05:00

87 lines
2.9 KiB
Go

package handlers
import (
"bytes"
"io"
"net/http"
"strconv"
"atcr.io/pkg/appview/installscript"
)
// InstallHandler handles the /install page
type InstallHandler struct {
BaseUIHandler
}
func (h *InstallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
meta := NewPageMeta(
"Install "+h.ClientShortName+" Credential Helper - "+h.ClientShortName,
"Install the "+h.ClientShortName+" credential helper to push and pull containers using your AT Protocol identity",
).WithCanonical("https://" + h.SiteURL + "/install").
WithSiteName(h.ClientShortName)
data := struct {
PageData
Meta *PageMeta
}{
PageData: NewPageData(r, &h.BaseUIHandler),
Meta: meta,
}
if err := h.Templates.ExecuteTemplate(w, "install", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// InstallScriptHandler serves the credential helper install scripts. They used
// to be static files, which meant every deployment served atcr.io's copy:
// it installed docker-credential-atcr and told the user to point Docker's
// credHelpers at atcr.io regardless of which registry they were setting up.
// Rendering from config keeps one script for every brand.
//
// The scripts are public: `curl -fsSL <site>/static/install.sh | bash` has to
// work before the user has any credentials at all.
type InstallScriptHandler struct {
BaseUIHandler
}
// params builds the render inputs. RegistryHost is the credHelpers key, which
// must be the *registry* host Docker authenticates against, not the web UI
// host the script was downloaded from: on Seamark those differ.
func (h *InstallScriptHandler) params() installscript.Params {
return installscript.Params{
Brand: h.CredHelper,
RegistryHost: h.RegistryURL,
SiteHost: h.SiteURL,
}
}
// ServeShell renders install.sh.
func (h *InstallScriptHandler) ServeShell(w http.ResponseWriter, r *http.Request) {
h.serve(w, "text/x-shellscript; charset=utf-8", installscript.RenderShell)
}
// ServePowerShell renders install.ps1.
func (h *InstallScriptHandler) ServePowerShell(w http.ResponseWriter, r *http.Request) {
h.serve(w, "text/plain; charset=utf-8", installscript.RenderPowerShell)
}
func (h *InstallScriptHandler) serve(w http.ResponseWriter, contentType string, render func(io.Writer, installscript.Params) error) {
// Render to a buffer first: a mid-stream template error would otherwise
// leave a truncated script that a piped `| bash` would happily execute.
var buf bytes.Buffer
if err := render(&buf, h.params()); err != nil {
http.Error(w, "failed to render install script", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
// Short cache: the body now depends on this deployment's config, so it
// must not be pinned for a year the way the static file was.
w.Header().Set("Cache-Control", "public, max-age=300")
_, _ = w.Write(buf.Bytes())
}