package bootstrap import ( "context" "fmt" "net/http" "os" "strings" "time" ) // Raising the package registry, before the base is built. // // The base (mesh-tools) resolves the SDK by version from the mesh's package registry rather than // cloning it from a git URL (novox/hq ADR 0076, issue 053). So the registry has to answer, and the // SDK has to be in it, before the base build runs. That is a pivot like the control plane's: gitea's // SERVER is raised directly here, on the foundation's own postgres, and adopted as an ordinary module // only after the base exists (which is what lets its provisioner image — built on the base — run). // // Nothing here is the steady state. It is the smallest set of acts that puts a working npm registry // in front of the base build: a database, a server, an admin, one org, the builder's own account, // and the SDK published under it. The gitea MODULE, installed after the base, takes all of this over. const ( // foundationStore is the foundation's postgres container — the mesh's own memory, raised from the // bundle. gitea's bootstrap database lives here too, so a mesh runs one postgres (issue 051). foundationStore = "mesh-store" // giteaBootstrap is the gitea server raised directly at genesis, before gitea is a module. giteaBootstrap = "mesh-gitea-server" // giteaImage is the same upstream image the gitea module runs, pinned identically so the module // adopts the running server rather than replacing it. giteaImage = "gitea/gitea@sha256:dfc61e347c8b582df918f4556401bf2cecdfbdb56c5282ae9488dd76fca3e41c" // packagesOrg is the npm owner: every module consumes `@novox/*` from this gitea org. packagesOrg = "novox" // packagesTeam is the org team whose members may read and write the org's packages. packagesTeam = "packages" // giteaAdminUser is the admin the bootstrap creates and the provisioner later authenticates as. giteaAdminUser = "mesh-admin" // builderGiteaUser is the gitea account the builder publishes and pulls with at genesis. It is // the `as` the builder's static package binding names. builderGiteaUser = "mesh-builder" // giteaDBRole/giteaDBName is gitea's own database in the foundation store. giteaDBRole = "mesh_gitea" giteaDBName = "mesh_gitea" // defaultGiteaPort is where the raised server answers on the machine unless the node gave the // package registry another port (novox/hq ADR 0100). defaultGiteaPort = 3000 ) // RaisePackageRegistry puts a working npm registry in front of the base build. It is idempotent: // every step tolerates having been done, because genesis is safe to run again. func RaisePackageRegistry(ctx context.Context, o Options, d Deps, control controlPlane, say func(string)) error { run := d.Run // The passwords this pivot mints, kept once so a re-run is the same run: gitea already holds // them, so regenerating would lock the mesh out of the forge it just raised. dbPassword, adminPassword, builderPassword, err := packagePasswords() if err != nil { return err } say(" seeding gitea's database in the foundation store") ports := o.Ports.orDefaults() if err := seedGiteaDatabase(ctx, run, o.Timeout, dbPassword, say); err != nil { return err } say(" raising the gitea server on that database") if err := raiseGiteaServer(ctx, run, o.Timeout, dbPassword, ports, say); err != nil { return err } say(" waiting for gitea to answer") base := fmt.Sprintf("http://127.0.0.1:%d", ports.Packages) if err := waitForGitea(ctx, d, o, base, say); err != nil { return err } say(" creating the gitea admin") if err := createGiteaAdmin(ctx, run, o.Timeout, adminPassword, say); err != nil { return err } admin := &giteaAdmin{base: base, user: giteaAdminUser, password: adminPassword, client: &http.Client{Timeout: o.Timeout}} say(" ensuring the npm org, its package team, and the builder's account") if err := admin.ensureOrg(ctx, packagesOrg); err != nil { return err } teamID, err := admin.ensureTeam(ctx, packagesOrg, packagesTeam) if err != nil { return err } if err := admin.ensureUser(ctx, builderGiteaUser, builderPassword); err != nil { return err } if err := admin.addToTeam(ctx, teamID, builderGiteaUser); err != nil { return err } say(" delivering the builder its registry credential") if err := deliverBuilderNpm(ctx, o, control, builderPassword, say); err != nil { return err } return nil } // seedGiteaDatabase creates gitea's role and database inside the foundation postgres, the same way // the foundation creates its own — psql run through the store container (the map's Route B). The role // is created before the database because the database is owned by it. Both are tolerant of already // existing, so a re-run changes nothing. func seedGiteaDatabase(ctx context.Context, run Runner, timeout time.Duration, password string, say func(string)) error { asking, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Single statements through psql -c, not one script: CREATE DATABASE cannot run in a // transaction and \gexec does not parse through -c. The password is base64url, so it carries no // quote or backslash to escape inside a SQL literal. psql := func(sql string) (string, error) { return run(asking, "docker", "exec", foundationStore, "psql", "-U", "postgres", "-tAc", sql) } // The role: create it, and if it is already there (create fails) reset its password so a re-run // converges on this run's credential. create := fmt.Sprintf("CREATE ROLE %s LOGIN PASSWORD '%s'", giteaDBRole, password) if _, err := psql(create); err != nil { alter := fmt.Sprintf("ALTER ROLE %s LOGIN PASSWORD '%s'", giteaDBRole, password) if _, err := psql(alter); err != nil { return fmt.Errorf("could not create gitea's role in %s: %w", foundationStore, err) } } // The database: created only if absent, because CREATE DATABASE has no IF NOT EXISTS and a // second create is an error rather than a no-op. present, err := psql(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname='%s'", giteaDBName)) if err != nil { return fmt.Errorf("could not check for gitea's database in %s: %w", foundationStore, err) } if strings.TrimSpace(present) != "1" { if _, err := psql(fmt.Sprintf("CREATE DATABASE %s OWNER %s", giteaDBName, giteaDBRole)); err != nil { return fmt.Errorf("could not create gitea's database in %s: %w", foundationStore, err) } } return nil } // raiseGiteaServer starts the gitea server container against the foundation store. It runs on the // machine's own network, so `127.0.0.1` reaches the store where it publishes its port, and it binds // its own port there for the builder and this installer. Started if absent, left alone if present. func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, dbPassword string, ports FoundationPorts, say func(string)) error { asking, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Already there: a re-run does not raise a second one. `docker start` is a no-op on a running // container and revives a stopped one. if out, _ := run(asking, "docker", "inspect", "--format", "{{.Id}}", giteaBootstrap); strings.TrimSpace(out) != "" { _, _ = run(asking, "docker", "start", giteaBootstrap) return nil } env := []string{ "-e", "GITEA__database__DB_TYPE=postgres", // The store is reached on the shared network namespace's loopback. "-e", fmt.Sprintf("GITEA__database__HOST=127.0.0.1:%d", ports.Store), "-e", "GITEA__database__NAME=" + giteaDBName, "-e", "GITEA__database__USER=" + giteaDBRole, "-e", "GITEA__database__PASSWD=" + dbPassword, // Skip the install wizard: the mesh configures gitea, not a person at a browser. "-e", "GITEA__security__INSTALL_LOCK=true", // The forge answers on the machine's loopback, and its own links must say so: gitea's // package metadata hands npm a tarball URL built from ROOT_URL, and a client only sends its // stored credential to the host it was stored for. A default ROOT_URL of localhost is a // different host than the binding's 127.0.0.1, so the credential would not be sent. "-e", fmt.Sprintf("GITEA__server__ROOT_URL=http://127.0.0.1:%d/", ports.Packages), "-e", "USER_UID=1000", "-e", "USER_GID=1000", } if ports.Packages != defaultGiteaPort { // On the machine's network the server binds its own port, so a port given for it is // the one it is told to listen on. env = append(env, "-e", fmt.Sprintf("GITEA__server__HTTP_PORT=%d", ports.Packages)) } args := append([]string{ "run", "-d", "--name", giteaBootstrap, // Host network, like the control plane: it reaches the foundation store on the machine's // loopback (where the store publishes 5432) and answers on the machine's own 3000, which is // where mesh-bootstrap and the builder's build containers look for it. "--network", "host", "--restart", "unless-stopped", }, env...) args = append(args, giteaImage) if _, err := run(asking, "docker", args...); err != nil { return fmt.Errorf("could not raise the gitea server: %w", err) } return nil } // waitForGitea polls gitea's version endpoint until it answers or the wait runs out. A container // that is up is not a forge that serves; `/api/v1/version` is the question whose answer means it is. func waitForGitea(ctx context.Context, d Deps, o Options, base string, say func(string)) error { deadline := time.Now().Add(o.Wait) url := base + "/api/v1/version" for { status, _, err := d.Fetch(ctx, url) if err == nil && status == http.StatusOK { return nil } if time.Now().After(deadline) { return fmt.Errorf("gitea did not answer at %s within %s", url, o.Wait) } select { case <-ctx.Done(): return ctx.Err() case <-time.After(2 * time.Second): } } } // createGiteaAdmin creates the mesh's gitea admin through the server's own CLI. Tolerant of the // admin already existing, because a re-run must not fail on it — and it resets the password every // run, so a rotated admin secret takes. func createGiteaAdmin(ctx context.Context, run Runner, timeout time.Duration, password string, say func(string)) error { asking, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Create once, with the password kept across re-runs, and do not follow with change-password: // change-password re-enables must-change-password, which then refuses every API call as // "you must change your password". Tolerant of "already exists", because the kept password // means the existing admin is already the one this run authenticates as. create := fmt.Sprintf( "gitea admin user create --admin --username %s --email %s@localhost --password %q --must-change-password=false", giteaAdminUser, giteaAdminUser, password) if _, err := run(asking, "docker", "exec", "-u", "git", giteaBootstrap, "sh", "-c", create+" || true"); err != nil { return fmt.Errorf("could not create the gitea admin: %w", err) } return nil } // packagePasswords loads the pivot's three passwords, minting and keeping them the first time. Kept // on the machine because gitea, once raised, holds them: a second genesis that minted fresh ones // would raise a forge it cannot then log into. func packagePasswords() (db, admin, builder string, err error) { const dir = "/var/lib/mesh/packages" const file = dir + "/bootstrap.env" if raw, e := os.ReadFile(file); e == nil { vals := map[string]string{} for _, line := range strings.Split(string(raw), "\n") { if k, v, ok := strings.Cut(strings.TrimSpace(line), "="); ok { vals[k] = v } } if vals["DB"] != "" && vals["ADMIN"] != "" && vals["BUILDER"] != "" { return vals["DB"], vals["ADMIN"], vals["BUILDER"], nil } } db, admin, builder = newPassword(), newPassword(), newPassword() if err = os.MkdirAll(dir, 0o700); err != nil { return "", "", "", err } content := fmt.Sprintf("DB=%s\nADMIN=%s\nBUILDER=%s\n", db, admin, builder) if err = os.WriteFile(file, []byte(content), 0o600); err != nil { return "", "", "", err } return db, admin, builder, nil } // deliverBuilderNpm seals the builder's registry password to this node as its `npm-password` // own-secret, the same way the control plane's store connections are delivered — carry the value in, // `secret accept`, and the next push writes it sealed where the builder reads it. func deliverBuilderNpm(ctx context.Context, o Options, control controlPlane, password string, say func(string)) error { at := "/accepting-npm-password" if err := control.carryingSecret(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil { return err } if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, "npm-password", "--from", at); err != nil { return err } // Push so the sealed secret reaches the builder, which restarts on it and comes back credentialed. if _, err := control.tell(ctx, "push", o.Node); err != nil { return err } return nil } // PublishTheSDK dispatches a build of the SDK to the builder. The builder has its registry // credential by now, so the build's `npm publish` authenticates; the artifact is a `package`, built // on a public base, so this needs no toolchain — which is the whole point of doing it before the base. func PublishTheSDK(ctx context.Context, o Options, control controlPlane, say func(string)) error { if o.SDKSource.Repository == "" { return fmt.Errorf("raising the registry needs --sdk-source: the SDK is built from its own " + "repository, and an installer told nothing cannot know where that is") } say(" publishing " + o.SDKSource.Repository + " at " + refOr(o.SDKSource.Ref)) _, err := control.within(buildWait).tell(ctx, "build", o.SDKSource.Repository, "--ref", refOr(o.SDKSource.Ref), "--wait", "1200s") return err }