carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "os"
10 "path/filepath"
11 "sort"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/activator"
15 "github.com/carlosframework/platform/internal/moves"
16 "github.com/carlosframework/platform/internal/provision"
17)
18
19// Version skew of the env report and pin (home part 2, Task 2b fix round
20// 3). A v1 report carries no instance layer names, so it cannot say
21// whether a box variable was the instance's or the box's: an older
22// producer and a newer one with no instance-set box variables look the
23// same. A v2 participant therefore refuses a v1 report at T3 and a v1 pin
24// at the probe, and a v2 pin is in a form no v1 participant's digest can
25// equal. The older participant is emulated here by its own rule: f4f51ba6's
26// digest, every variable but the agent-owned and box variables, in the
27// "sha256:" form.
28
29// v1EnvDigest is a v1 participant's digest of a report's variables.
30func v1EnvDigest(r moves.EnvReport) string {
31 var names []string
32 for k := range r.Vars {
33 if !moves.AgentOwnedVars[k] && !moves.BoxVars[k] {
34 names = append(names, k)
35 }
36 }
37 sort.Strings(names)
38 h := sha256.New()
39 for _, k := range names {
40 fmt.Fprintf(h, "%s=%s\n", k, r.Vars[k])
41 }
42 return "sha256:" + hex.EncodeToString(h.Sum(nil))
43}
44
45// asV1Source rewrites the source's published report as a v1 source writes
46// it: version 1, no instance layer names, the v1 digest.
47func asV1Source(t *testing.T, h *phase1Harness) {
48 t.Helper()
49 ctx := context.Background()
50 rec := h.read(t, p1Host)
51 r := h.sourceReport(t)
52 r.Env.V, r.Env.Instance = 1, nil
53 r.Env.Digest = v1EnvDigest(r.Env)
54 body, err := json.Marshal(r)
55 if err != nil {
56 t.Fatal(err)
57 }
58 if err := moves.PutReplace(ctx, h.s, moves.SourceReportKey(moves.ReportPrefix(rec.Account, rec.App, rec.Host, rec.Move.ID)), body); err != nil {
59 t.Fatal(err)
60 }
61}
62
63const t3OldReportWant = "the source reported its launch environment in an older format"
64
65// TestT3RefusesAnOlderSourceReport: a v1 source with a v2 target. The
66// target refuses the source's v1 report at T3, before comparing, named to
67// named and named to home.
68func TestT3RefusesAnOlderSourceReport(t *testing.T) {
69 t.Parallel()
70 for name, opts := range map[string][]p1Option{
71 "named to named": {realLaunchEnv},
72 "named to home": {intoHome, realLaunchEnv},
73 } {
74 t.Run(name, func(t *testing.T) {
75 h := newPhase1Harness(t, opts...)
76 h.start(t, "manual")
77 h.pass(t)
78 asV1Source(t, h)
79 h.targetPass(t, h.targetCfg())
80 r := h.targetReport(t)
81 if t3 := r.Checks[moves.CheckEnv]; t3.OK || t3.Reason != t3OldReportWant {
82 t.Fatalf("T3 on a v1 source report: %+v", t3)
83 }
84 if r.Inputs.EnvReport != "" {
85 t.Fatalf("a refused T3 bound the source's env report: %q", r.Inputs.EnvReport)
86 }
87 })
88 }
89}
90
91// TestProbeRefusesAnOlderPin: a v1 source with a v2 target, and the
92// instance's HOSTNAME removed after the pin. The source's .env set
93// HOSTNAME=a, which a v1 pin leaves out; after the pin the target's .env
94// loses it and the target inherits HOSTNAME=b from its box, which the v2
95// rule also leaves out. The two digests would agree on a changed env; the
96// v2 target refuses the v1 pin instead, named to named and named to home.
97func TestProbeRefusesAnOlderPin(t *testing.T) {
98 for name, opts := range map[string][]p1Option{
99 "named to named": {realLaunchEnv},
100 "named to home": {intoHome, realLaunchEnv},
101 } {
102 t.Run(name, func(t *testing.T) {
103 h := newCloseHarness(t, append([]p1Option{sourceEnvFile("APP_MODE=prod\nHOSTNAME=a\n")}, opts...)...)
104 p := h.productionProbe(t)
105 v1 := v1EnvDigest(h.sourceReport(t).Env)
106 if _, err := provision.SetRecordMove(context.Background(), h.s, "bab", "app", p1Host, func(rec *provision.Record) error {
107 rec.Move.Inputs.EnvReport = v1
108 return nil
109 }); err != nil {
110 t.Fatal(err)
111 }
112 if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte("APP_MODE=prod\n"), 0o600); err != nil {
113 t.Fatal(err)
114 }
115 t.Setenv("HOSTNAME", "b")
116 h.land(t)
117 if len(p.admissions) != 0 {
118 t.Fatalf("a v1 pin admitted a changed env: %+v", p.admissions)
119 }
120 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackConfigChanged || m.State == provision.MoveCommitted {
121 t.Fatalf("the landing on a v1 pin: state %s reason %q", m.State, m.Reason)
122 }
123 })
124 }
125}
126
127// TestOlderTargetCannotMatchANewerPin: a v2 source with a v1 target. A v1
128// target judges its own launch by the v1 digest; whatever its env, that
129// digest never equals a v2 pin, so the older target refuses, fail closed.
130// Both directions of a box variable changing after the pin:
131// - the source's HOSTNAME is inherited (left out of the pin) and the
132// target's .env adds HOSTNAME=b;
133// - the source's .env sets HOSTNAME=a (pinned) and the target's .env
134// loses it, inheriting HOSTNAME=b.
135func TestOlderTargetCannotMatchANewerPin(t *testing.T) {
136 for name, c := range map[string]struct {
137 srcFile, tgtFile string
138 }{
139 "the target adds an instance HOSTNAME": {"APP_MODE=prod\n", "APP_MODE=prod\nHOSTNAME=b\n"},
140 "the target loses the instance HOSTNAME": {"APP_MODE=prod\nHOSTNAME=a\n", "APP_MODE=prod\n"},
141 } {
142 t.Run(name, func(t *testing.T) {
143 t.Setenv("HOSTNAME", "a")
144 h := newCloseHarness(t, sourceEnvFile(c.srcFile), intoHome, realLaunchEnv)
145 pin := h.read(t, p1Host).Move.Inputs.EnvReport
146 if !moves.CurrentEnvPin(pin) {
147 t.Fatalf("the source's pin is not v2: %q", pin)
148 }
149 if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte(c.tgtFile), 0o600); err != nil {
150 t.Fatal(err)
151 }
152 t.Setenv("HOSTNAME", "b")
153 env, _, err := (&activator.ExecBackend{DataDir: h.tdir, Region: h.dst}).LaunchEnvironment(h.route)
154 if err != nil {
155 t.Fatal(err)
156 }
157 key, err := moves.ReportKeyFromSecrets(h.cfg.SecretsKey)
158 if err != nil {
159 t.Fatal(err)
160 }
161 if got := v1EnvDigest(moves.ReportEnv(key, env)); got == pin {
162 t.Fatalf("a v1 target's digest of a changed env matches the v2 pin %q", pin)
163 }
164 })
165 }
166}
167