after_start deploy fields: asked after the start by this controller, still required for older ones (R-1028/R-1045, security review of catalog 0188430)
gates / gates (push) Successful in 47s

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-15 10:32:28 +02:00
parent 372d6e5994
commit f327bc53a9
4 changed files with 43 additions and 12 deletions
+5
View File
@@ -41,6 +41,11 @@ Not released yet (bake → vouch → floor, `09` §3 decision 247). Design: felh
- **„Kész" never opens an unclaimed Plex (R-1045)** — the controller asks Plex itself (`/identity` with the JSON Accept
header) both after the claim and on the household's press. The live template carries NO probe: a controller before
0.313.0 would send no Accept header and refuse the press forever. `TestPlexDonePress_RefusedWhileUnclaimed` (red-proved).
- **`after_start:` deploy fields (R-1028/R-1045; a background security review of catalog `0188430`)** — this controller
does not ask such a field in the wizard and accepts it empty (Plex's claim code is asked on the Plex page after the
start); an older controller ignores the key and keeps the field required. The first catalog push had made the code
optional for EVERY controller, which let an old controller's „Kész" open an unclaimed Plex. `TestAfterStartField_*`
(red-proved).
- **Old Sonarr/Radarr installs repaired by themselves (R-1047)** — only when /media is no longer bound: same-path root
folder, `moveFiles:false` editor, rescan, old root removed, Radarr's collections too. `TestRepairOldRoot_K7`
(red-proved: moveFiles=true is caught).
+2 -2
View File
@@ -390,8 +390,8 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
}
}
// Validate required fields
if field.Required && value == "" {
// Validate required fields (an after_start field is asked after the start — R-1028)
if field.Required && value == "" && !field.AfterStart {
clearDeploying()
return "", util.MsgErrorf(ErrRequiredField, "err.stacks.field_required", field.Label, field.EnvVar)
}
@@ -1,6 +1,8 @@
package stacks
import (
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"strings"
@@ -169,3 +171,19 @@ func TestR1031_MediaAppIsNotAskedForADrive(t *testing.T) {
t.Fatal("an app without the marker must still be asked for its drive")
}
}
// R-1028/R-1045: an `after_start` field (Plex's claim code) is not in this controller's wizard and may be empty here;
// the same template stays REQUIRED for an older controller (it ignores the key). COMPANION RED-PROOF: drop the
// AfterStart skip in UserFacingFields → this test fails. Restored.
func TestAfterStartField_NotAskedAtInstall(t *testing.T) {
m := Metadata{DeployFields: []DeployField{{EnvVar: "PLEX_CLAIM", Type: "text", Required: true, AfterStart: true}, {EnvVar: "SUBDOMAIN", Type: "subdomain"}}}
for _, f := range m.UserFacingFields() {
if f.EnvVar == "PLEX_CLAIM" {
t.Fatal("the claim code is asked at install — the 4-minute race is back (R-1028)")
}
}
var meta Metadata
if err := yaml.Unmarshal([]byte("deploy_fields:\n - env_var: PLEX_CLAIM\n type: text\n required: true\n after_start: true\n"), &meta); err != nil || !meta.DeployFields[0].AfterStart || !meta.DeployFields[0].Required {
t.Fatalf("the template shape must parse as required + after_start: %+v %v", meta.DeployFields, err)
}
}
+18 -10
View File
@@ -195,16 +195,21 @@ type ResourceHints struct {
// DeployField defines one configuration field shown during first deployment.
type DeployField struct {
EnvVar string `yaml:"env_var" json:"env_var"`
Label string `yaml:"label" json:"label"`
Type string `yaml:"type" json:"type"` // domain, subdomain, secret, password, path, text, select, boolean
Generate string `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin"
Default string `yaml:"default" json:"default"`
Required bool `yaml:"required" json:"required"`
Placeholder string `yaml:"placeholder" json:"placeholder"`
Description string `yaml:"description" json:"description"`
LockedAfterDeploy bool `yaml:"locked_after_deploy" json:"locked_after_deploy"`
Options []SelectOption `yaml:"options" json:"options,omitempty"`
EnvVar string `yaml:"env_var" json:"env_var"`
Label string `yaml:"label" json:"label"`
Type string `yaml:"type" json:"type"` // domain, subdomain, secret, password, path, text, select, boolean
Generate string `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin"
Default string `yaml:"default" json:"default"`
Required bool `yaml:"required" json:"required"`
Placeholder string `yaml:"placeholder" json:"placeholder"`
Description string `yaml:"description" json:"description"`
LockedAfterDeploy bool `yaml:"locked_after_deploy" json:"locked_after_deploy"`
// AfterStart (v0.313.0, R-1028/R-1045): this controller asks for the value AFTER the app started (Plex's claim code on
// the Plex page), so the wizard does not show the field and an empty value is allowed. An OLDER controller ignores the
// key and keeps the field — required — so a template can stay safe for both (a code made optional for everyone would
// let an old controller's „Kész" open an unclaimed Plex). Pinned by TestAfterStartField_*.
AfterStart bool `yaml:"after_start,omitempty" json:"after_start,omitempty"`
Options []SelectOption `yaml:"options" json:"options,omitempty"`
// DataKey marks a field as a DATA-ENCRYPTING key (e.g. AdventureLog's "Titkosítási kulcs"):
// the app encrypts stored data with it, so regenerating it would render restored data
// unreadable. It is a fail-closed annotation only — the recovery unit never stores secrets;
@@ -539,6 +544,9 @@ func (m *Metadata) UserFacingFields() []DeployField {
if m.MediaLibrary != nil && f.EnvVar == "HDD_PATH" {
continue
}
if f.AfterStart {
continue
}
if f.Type != "domain" && f.Type != "secret" {
fields = append(fields, f)
}