package bootstrap import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" ) // The controller as a process, raised at genesis as a container (novox/hq issue 213, issue 223). // // **The mesh runs the controller as a Go bundle the host starts as a process; genesis cannot.** A // process's bundle is fetched from the mesh's artifact store, which genesis raises long after the // controller, and a Go bundle is compiled in a toolchain the mesh builds later still. So genesis // pivots to the controller as it always has — an image it built from the controller's own // Dockerfile, run as a container the temporary controller composes — and the first time the mesh // builds the controller from its repository, the controller's own declaration is the process, which // names that container under `replaces`, and the host hands over: the process is started, seen up, // and only then is the container removed (mesh-host `replaces`, issue 213). // // **The container is genesis's shape, not the manifest's.** The manifest declares only the process. // From it genesis takes what the process is given — its environment, which is host paths and words — // and the id the process replaces; the container around it is written here: the image genesis built, // the host's network, and every host path the environment names mounted at the same path read-only. // It runs as the image's own unprivileged user (65534), so the secrets belong to that number until // the process's account takes them over — the image is FROM scratch and knows no account by name. Its // id is the one the process replaces, so the first declaration the mesh composes for this machine // hands this container over rather than leaving two controllers running. // genesisUser is who the genesis container runs as — the image's own USER — and who its secrets // belong to until the process takes them over: the image has no passwd to look an account up in. const genesisUser = "65534:65534" // ProcessForm is the controller's process, as its manifest declares it, and the container id that // process replaces. Found is false for a manifest in the image form — an older controller — which // genesis installs as it always did. type ProcessForm struct { Found bool Process string // the process resource's id Replaces string // the id of the container genesis raises in its place } // processFormOf finds the controller's process in its manifest: a process resource running a bundle // the module builds, saying which one resource it replaces. func processFormOf(manifest []byte) (ProcessForm, error) { var m struct { Build *struct { Artifacts []struct { Name string `json:"name"` Kind string `json:"kind"` } `json:"artifacts"` } `json:"build"` Resources []map[string]any `json:"resources"` } if err := json.Unmarshal(manifest, &m); err != nil { return ProcessForm{}, fmt.Errorf("the %s module's manifest is not readable: %w", ControlPlaneModule, err) } kinds := map[string]string{} if m.Build != nil { for _, a := range m.Build.Artifacts { kinds[a.Name] = a.Kind } } for _, kind := range kinds { if kind == "image" { return ProcessForm{}, nil // the image form: the builder builds it, as before } } var found []ProcessForm for _, r := range m.Resources { if r["type"] != "process" || kinds[fmt.Sprint(r["artifact"])] != "bundle" { continue } if once, _ := r["run-once"].(bool); once { continue } replaces, _ := r["replaces"].([]any) if len(replaces) != 1 { return ProcessForm{}, fmt.Errorf( "the %s module runs as the process %v and says it replaces %v. Genesis raises the "+ "controller as a container that process takes over, so the process names exactly "+ "one resource it replaces — the id genesis gives the container", ControlPlaneModule, r["id"], r["replaces"]) } found = append(found, ProcessForm{Found: true, Process: fmt.Sprint(r["id"]), Replaces: fmt.Sprint(replaces[0])}) } if len(found) != 1 { return ProcessForm{}, fmt.Errorf( "the %s module builds no image and runs %d process(es) of its own; genesis raises one "+ "controller, from the process its manifest declares", ControlPlaneModule, len(found)) } return found[0], nil } // genesisForm is the manifest genesis registers: the controller's own manifest, its process // replaced by the container genesis runs in its place, under the id the process replaces, running // the image genesis built. Resolved as a build would resolve it — no build section, the image named // — because that is what the temporary controller is handed. func genesisForm(manifest []byte, form ProcessForm, image string) ([]byte, error) { var m map[string]any if err := json.Unmarshal(manifest, &m); err != nil { return nil, err } resources, _ := m["resources"].([]any) var out []any for _, raw := range resources { r, _ := raw.(map[string]any) if r == nil || r["id"] != form.Process { out = append(out, raw) continue } env, _ := r["env"].(map[string]any) var volumes []any for _, key := range sortedAnyKeys(env) { value := fmt.Sprint(env[key]) if strings.HasPrefix(value, "/") || strings.HasPrefix(value, "${dir:") { volumes = append(volumes, value+":"+value+":ro") } } container := map[string]any{ "id": form.Replaces, "type": "container", "name": ControlPlaneModule, "image": image, "network": "host", "args": []any{"serve"}, } if len(env) > 0 { container["env"] = env } if len(volumes) > 0 { container["volumes"] = volumes } out = append(out, container) } m["resources"] = out // What the container reads must be readable by who it runs as. The process's account owns them // once the process takes over, and the host gives them to it in the same apply. m["secrets-owner"] = genesisUser // **Nothing to prepare at genesis.** The temporary controller — the same commit — migrated the // stores when the foundation raised it, and a preparation step is derived from a resource running // an artifact the module built, which a pinned image is not: the controller refuses `prepares` // with nothing to run it in. The process prepares the stores itself when it takes over. delete(m, "prepares") delete(m, "build") var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) if err := enc.Encode(m); err != nil { return nil, err } return bytes.TrimSpace(b.Bytes()), nil } func sortedAnyKeys(m map[string]any) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } // cloneAt fetches the controller's repository at the commit genesis builds, into a directory of // this machine's, with the carried builder's git — the machine is not assumed to have one. Returns // the module's directory and the commit that was checked out. func cloneAt(ctx context.Context, run Runner, builderTag string, source Source, into string) (string, string, error) { git := func(args ...string) (string, error) { return run(ctx, "docker", append([]string{"run", "--rm", "-v", into + ":/ws", "--entrypoint", "git", builderTag}, args...)...) } if _, err := git("clone", "--quiet", source.Repository, "/ws/src"); err != nil { return "", "", fmt.Errorf("cloning %s: %w", source.Repository, err) } if _, err := git("-C", "/ws/src", "checkout", "--quiet", "--detach", source.Ref); err != nil { return "", "", fmt.Errorf("checking out %s: %w", shortRef(source.Ref), err) } commit, err := git("-C", "/ws/src", "rev-parse", "HEAD") if err != nil { return "", "", err } return filepath.Join(into, "src", source.Path), strings.TrimSpace(commit), nil } // buildGenesisImage builds the controller's image from its own Dockerfile (the one `make image` // uses), on this machine, and names it by the digest of its own configuration, as the builder did. func buildGenesisImage(ctx context.Context, run Runner, dir string) (string, error) { if _, err := os.Stat(filepath.Join(dir, "Dockerfile")); err != nil { return "", fmt.Errorf("the %s repository has no Dockerfile, so genesis has no image to raise "+ "the controller from: %w", ControlPlaneModule, err) } out, err := run(ctx, "docker", "build", "--quiet", dir) if err != nil { return "", fmt.Errorf("building the %s image: %w", ControlPlaneModule, err) } image := strings.TrimSpace(out) if i := strings.LastIndex(image, "\n"); i >= 0 { image = strings.TrimSpace(image[i+1:]) } if !strings.HasPrefix(image, "sha256:") { return "", fmt.Errorf("docker build said %q, which is not an image id", firstLine(out)) } return image, nil }