Skip to content

feat(scaffolder): add create-tenant-chart software template#132

Open
quiiver wants to merge 24 commits into
mainfrom
feat/scaffolder-tenant-chart
Open

feat(scaffolder): add create-tenant-chart software template#132
quiiver wants to merge 24 commits into
mainfrom
feat/scaffolder-tenant-chart

Conversation

@quiiver

@quiiver quiiver commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Adds a create-tenant-chart Backstage software template plus a backend scaffolder module (@internal/plugin-scaffolder-backend-module-mozcloud) that adds a new Helm chart to an existing tenant. For a selected tenant (System) it:

  1. Fetches the tenant YAML from mozilla/global-platform-admin.
  2. Reads its function + environments (mozcloud:tenant:read).
  3. Renders a new chart from the private mozilla/mozcloud-tenant-skeleton via the copier CLI (run:copier, chart mode).
  4. Opens a PR to <function>-infra with the rendered chart.
  5. Registers the chart's deployment values in the tenant YAML (mozcloud:tenant:add-chart) and opens a PR to global-platform-admin.

Key components

  • plugins/scaffolder-backend-module-mozcloud — three custom actions (mozcloud:tenant:read, run:copier, mozcloud:tenant:add-chart) with pure, unit-tested cores (26 tests). Registered on the scaffolder via coreServices.rootConfigScmIntegrationsDefaultGithubCredentialsProvider.
  • scaffolder-templates/create-tenant-chart/template.yaml — the template. PR branches are keyed <user>-<tenant>-<chart>; PR titles follow conventional commits (feat(<tenant>): …).
  • Template discoveryapp-config.yaml adds Template to catalog.rules and a ../../scaffolder-templates/*/template.yaml file location (relative to the backend cwd in yarn start).
  • Image — copier installed into the backend image from packages/backend/requirements.txt (venv at /opt/pyenv), git installed (copier shells out to it), and scaffolder-templates/ bundled in.
  • Sidebar — restored the "Create..." nav item; removed the unused create-rfc template.

GitHub auth model

  • Local dev: the github OAuth provider is off, so the picker's requestUserCredentials is inert and all GitHub steps fall back to the integration token (GITHUB_TOKEN=$(gh auth token)). The template's "Open the PRs as me" toggle lets you skip the OAuth prompt locally.
  • Stage/prod: per-user OAuth mints USER_OAUTH_TOKEN so PRs are authored by the initiator.

Deploy prerequisites (must be done before this works in stage/prod)

  1. catalog.locations override in GKE config — the container cwd is /app, so the prod config must target scaffolder-templates/*/template.yaml (not the ../../ dev path). Templates are bundled in the image.
  2. GitHub OAuth for per-user PRs — configure auth.providers.github.production with AUTH_GITHUB_CLIENT_ID/SECRET. Note the moz-backstage-app GitHub App is currently read-only; per-user PR creation requires it to have contents: write + pull_requests: write.

Testing

  • 26 unit tests in the scaffolder module (YAML merge, tenant parsing, copier invocation/token/.git URL/credential fallback, log redaction).
  • yarn tsc, yarn lint, yarn prettier:check clean.
  • Ran end-to-end locally (both PRs created) against a real tenant.

Out of scope

  • The skeleton renders app_code as the chart name rather than the tenant name; fixing that is a mozcloud-tenant-skeleton change, intentionally deferred.

quiiver added 24 commits July 14, 2026 09:08
Add mozcloud:tenant:read, a pure parseTenantContext(yamlText) that extracts
the tenant function and unique realm environment names, wrapped in a
zod-based createReadTenantAction() that also derives copierEnvironments
(intersection with dev/stage/prod, falling back to all three).
Add run:copier, a scaffolder action that execs the copier CLI in chart
mode: writes a --data-file (kind=chart, name, function, environments)
to the workspace, injects the GitHub token into the HTTPS clone URL,
and streams the run to ctx.logger via executeShellCommand. Register it
alongside mozcloud:tenant:read and mozcloud:tenant:add-chart in a new
scaffolderModuleMozcloud backend module, and wire that module into the
backend.
…overy

Adds the Backstage software template that renders a new chart from
mozcloud-tenant-skeleton for an existing tenant and opens PRs to both
<function>-infra and global-platform-admin, and enables Template
discovery (catalog.rules + catalog.locations) so it shows up in the
catalog.
The create-tenant-chart scaffolder uses auth.providers.github
(requestUserCredentials) to author PRs as the initiator. Document the
two env vars: optional locally (provider degrades gracefully in dev),
REQUIRED before the app-config block ships to any non-dev environment
(the auth backend hard-fails to start without them when NODE_ENV != development).
…nd cwd

The catalog FileReaderProcessor globs the location target relative to the
backend's process.cwd() (packages/backend under `yarn start`), so
`scaffolder-templates/*/template.yaml` matched nothing and the template never
appeared under /create. Prefix `../../` to reach the repo root, matching the
stock `../../examples/...` convention.
Ships scaffolder-templates/ into the image (/app/scaffolder-templates) so
templates can be discovered via a catalog.locations file target in production.
The container cwd is /app, so the GKE production config must target
`scaffolder-templates/*/template.yaml` (not the `../../` dev path).
Clarify the hybrid GitHub auth model for create-tenant-chart:
- Local: set GITHUB_TOKEN=$(gh auth token); the scaffolder falls back to the
  integration token when no per-user OAuth token is present.
- Prod/stage: AUTH_GITHUB_CLIENT_ID/SECRET enable per-user PR authorship via
  requestUserCredentials.
Add an 'authenticateAsMe' boolean (default true) that gates the RepoUrlPicker
behind a schema dependency. When on (stage/prod), the picker mounts and
requestUserCredentials runs the OAuth popup for per-user PR authorship. When
off (local dev, where no GitHub OAuth provider is configured), the picker never
mounts so no popup fires; USER_OAUTH_TOKEN stays empty and the GitHub steps
fall back to the backend integration token.
…angs on auth

The run:copier action passed the (possibly empty) per-user token straight into
the clone URL, so with no USER_OAUTH_TOKEN (local dev) it cloned the private
mozcloud-tenant-skeleton unauthenticated and git blocked forever on an
interactive credential prompt.

- Resolve the clone token via the GitHub integration credential provider when
  no explicit token is given (mirrors fetch:plain:file / publish:github),
  extracted as testable resolveCloneToken().
- Set GIT_TERMINAL_PROMPT=0 / GCM_INTERACTIVE=never so a missing/invalid
  credential fails fast instead of hanging.
- Inject DefaultGithubCredentialsProvider via the module (rootConfig).
… as git

copier only treats a source as git when the URL starts with a known prefix
(incl. https://github.com/) or ends in .git. Injecting x-access-token userinfo
breaks the https://github.com/ prefix match, so copier treated the URL as a
local path and failed with 'Local template must be a directory.'. Append .git
(via ensureGitUrl) so the authed URL stays in git mode.
…es; restore Create nav; drop create-rfc

- PR branch names prefixed with the initiating user's local name
  (e.g. wstuckey-create-chart-<tenant>) via user.entity.metadata.name.
- PR titles follow conventional commits: feat(<tenant>): add/register <chart>.
- Restore the 'Create...' sidebar item (AddCircleOutline icon).
- Remove the create-rfc scaffolder template.
Key branches on user + tenant + chart so adding multiple charts to the same
tenant no longer collides on a shared branch name.
Review fixes:
- Add git to the backend image apt install. copier shells out to git to clone
  the template repo, and node:20-bookworm-slim does not ship it, so run:copier
  would fail at clone time in production.
- redactingLogger.child() now wraps the child logger too, so secret redaction
  can't be bypassed via .child(); add unit tests for the redaction paths.
copier records its source URL verbatim as _src_path in the committed
.copier-answers.yml, so injecting the token into the clone URL leaked it into
the generated PR. Authenticate the clone via an http.extraheader (GIT_CONFIG_*
env, github.com-scoped) with a bare URL instead, so no credential is ever
written to disk. Replaces injectToken with buildGitAuthEnv; adds a regression
test asserting the clone URL never contains a credential.
…dard

The add-chart action emitted `^<env>-.{40}$` (old 40-char, env-prefixed scheme).
Per Confluence 'Standards: Container Images' + WT-1419, paved-path tags are a
10-char short SHA on main pushes and a semver tag on releases. Emit
`^[0-9a-f]{10}$` for non-prod envs (track main) and `^v[0-9]+\.[0-9]+\.[0-9]+$`
for prod (track release tags), via imageRegexForEnv().
…ged in

The chart entry is now authored in the tenant YAML's own schema in
scaffolder-templates/create-tenant-chart/chart-values.yaml.njk
(globals.deployment.charts.<chart> and realms.<realm>.environments[].charts.<chart>).
The mozcloud:tenant:add-chart action renders it with Nunjucks against the
tenant's realms — each environment carrying its paved-path image_regex — and
drops the generated entries onto the matching tenant paths (comment-preserving
round-trip). A fetch:plain:file step stages the template into the workspace.
…token resolution

New github:ensureAuth action resolves the effective GitHub credential (per-user
token, else the integration credential) and verifies read access to the target
repos via the GitHub API, failing early with a clear message (401/403/404)
instead of deep in copier/publish. Token resolution (resolveGithubToken) +
owner/repo parsing move to a shared module reused by run:copier. The template
runs ensure-auth after reading the tenant, before cloning/PRs.
…picker

Replace the RepoUrlPicker + authenticateAsMe toggle with a frontend form
decorator (packages/app overrides/scaffolder.tsx, registered via
FormDecoratorBlueprint). On submit it calls githubAuthApi.getAccessToken —
initiating GitHub sign-in if needed — and stashes the token as
secrets.USER_OAUTH_TOKEN; when no GitHub provider is configured (local dev) it
swallows the error so the steps fall back to the integration token. The
template opts in via spec.formDecorators. github:ensureAuth still guards access.
…ution

The form decorator now handles obtaining the per-user token, so the
github:ensureAuth preflight guard is removed (action, template step, tests).
resolveGithubToken (the integration-token fallback used by run:copier) moves
back into runCopier.ts as its sole consumer.

Trade-off: an access problem now surfaces in the copier/publish step rather than
in an early preflight check.
…l popup)

The form decorator now only calls getAccessToken (which opens the sign-in
popup) when scaffolder.githubUserAuth is set — configured in stage/prod, left
unset locally. Locally the decorator returns early, so no popup appears and the
backend falls back to the integration token; in prod it prompts for the
per-user token. Adds the frontend config schema (packages/app/config.d.ts).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant