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" // giteaPort is where the raised server answers on the machine. giteaPort = 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") 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, say); err != nil { return err } say(" waiting for gitea to answer") base := fmt.Sprintf("http://127.0.0.1:%d", giteaPort) 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 joins the // store's network namespace so `127.0.0.1:5432` reaches postgres, and publishes its own port on the // machine so the builder and this installer can reach it. Started if absent, left alone if present. func raiseGiteaServer(ctx context.Context, run Runner, timeout time.Duration, dbPassword string, 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", "GITEA__database__HOST=127.0.0.1:5432", "-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/", giteaPort), "-e", "USER_UID=1000", "-e", "USER_GID=1000", } 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 }