diff --git a/modules/gitea/cmd/forge-account-provider/builder_test.go b/modules/gitea/cmd/forge-account-provider/builder_test.go new file mode 100644 index 00000000..9bfd0408 --- /dev/null +++ b/modules/gitea/cmd/forge-account-provider/builder_test.go @@ -0,0 +1,356 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + + "git.novox.be/novox/mesh-sdk/go/provider" + + "gitea/internal/forgehttp" +) + +// Issue 1481 (novox/hq ADR 0466 ยง3): the build modules' keys on one forge user that reads and pushes nothing. The forge +// is faked with the teams, members and keys it keeps, so a team's shape and a key's right can be read back. + +type forgeKey struct { + ID int64 + Title string + Key string + ReadOnly bool +} + +type teamForge struct { + mu sync.Mutex + users map[string]bool + teams map[string]map[string]any // by name: the shape last sent + ids map[string]int64 + members map[string][]string // by team name + keys map[string][]forgeKey + next int64 +} + +func newTeamForge() *teamForge { + return &teamForge{users: map[string]bool{}, teams: map[string]map[string]any{}, ids: map[string]int64{}, + members: map[string][]string{}, keys: map[string][]forgeKey{}, next: 10} +} + +var ( + teamRe = regexp.MustCompile(`^/api/v1/teams/(\d+)(?:/members/([^/]+))?$`) + userKeyRe = regexp.MustCompile(`^/api/v1/(?:admin/)?users/([^/]+)/keys(?:/(\d+))?$`) +) + +func (f *teamForge) teamByID(id string) string { + for name, n := range f.ids { + if fmt.Sprint(n) == id { + return name + } + } + return "" +} + +func (f *teamForge) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + defer f.mu.Unlock() + body, _ := io.ReadAll(r.Body) + var in map[string]any + _ = json.Unmarshal(body, &in) + reply := func(code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + if v != nil { + _ = json.NewEncoder(w).Encode(v) + } + } + p := r.URL.Path + if m := userKeyRe.FindStringSubmatch(p); m != nil { + switch r.Method { + case http.MethodGet: + out := []map[string]any{} + for _, k := range f.keys[m[1]] { + out = append(out, map[string]any{"id": k.ID, "title": k.Title, "key": k.Key}) + } + reply(200, out) + case http.MethodPost: + f.next++ + ro, _ := in["read_only"].(bool) + f.keys[m[1]] = append(f.keys[m[1]], forgeKey{ID: f.next, Title: in["title"].(string), Key: in["key"].(string), ReadOnly: ro}) + reply(201, map[string]any{"id": f.next}) + case http.MethodDelete: + var kept []forgeKey + for _, k := range f.keys[m[1]] { + if fmt.Sprint(k.ID) != m[2] { + kept = append(kept, k) + } + } + f.keys[m[1]] = kept + reply(204, nil) + } + return + } + if m := teamRe.FindStringSubmatch(p); m != nil { + name := f.teamByID(m[1]) + switch { + case name == "": + reply(404, map[string]any{"message": "no team"}) + case r.Method == http.MethodPatch && m[2] == "": + f.teams[name] = in + reply(200, map[string]any{}) + case r.Method == http.MethodPut: + for _, u := range f.members[name] { + if u == m[2] { + reply(204, nil) + return + } + } + f.members[name] = append(f.members[name], m[2]) + reply(204, nil) + default: + http.Error(w, "unexpected "+r.Method+" "+p, 400) + } + return + } + switch { + case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/users/"): + if !f.users[strings.TrimPrefix(p, "/api/v1/users/")] { + reply(404, map[string]any{"message": "no"}) + return + } + reply(200, map[string]any{"active": true}) + case r.Method == http.MethodPost && p == "/api/v1/admin/users": + f.users[in["username"].(string)] = true + reply(201, map[string]any{}) + case p == "/api/v1/orgs/novox/teams" && r.Method == http.MethodGet: + out := []map[string]any{} + for name, id := range f.ids { + out = append(out, map[string]any{"id": id, "name": name}) + } + reply(200, out) + case p == "/api/v1/orgs/novox/teams" && r.Method == http.MethodPost: + f.next++ + name := in["name"].(string) + f.ids[name], f.teams[name] = f.next, in + reply(201, map[string]any{"id": f.next}) + default: + http.Error(w, "unexpected "+r.Method+" "+p, 400) + } +} + +func (f *teamForge) titles(user string) []string { + f.mu.Lock() + defer f.mu.Unlock() + out := []string{} + for _, k := range f.keys[user] { + out = append(out, k.Title) + } + return out +} + +type teamRig struct { + t *testing.T + bus *fakeBus + forge *teamForge + svc *Service + runner *fakeRunner +} + +func newTeamRig(t *testing.T) *teamRig { + t.Helper() + forge := newTeamForge() + srv := httptest.NewServer(forge) + t.Cleanup(srv.Close) + dir := t.TempDir() + pw, host, origins := filepath.Join(dir, "pw"), filepath.Join(dir, "host"), filepath.Join(dir, "origins") + for p, c := range map[string]string{pw: "x\n", host: "novox.internal\n", origins: "https://git.novox.be\n"} { + if err := os.WriteFile(p, []byte(c), 0o600); err != nil { + t.Fatal(err) + } + } + runner := &fakeRunner{out: scanned} + svc := NewService(Config{Node: "novox", HostFile: host, OriginsFile: origins, Port: 222, + Forge: forgehttp.Client{URL: srv.URL, User: "mesh-admin", PasswordFile: pw}, Runner: runner, Log: func(string, ...any) {}}) + svc.loaded = true + svc.loadedFrom["mesh-build-go"], svc.loadedFrom["mesh-build-node"] = true, true + return &teamRig{t: t, bus: newBus(), forge: forge, svc: svc, runner: runner} +} + +func (r *teamRig) pass() []provider.Outcome { + r.t.Helper() + out, err := r.svc.Provider(r.bus).Pass(context.Background()) + if err != nil { + r.t.Fatal(err) + } + return out +} + +// bind makes module on node bind gitea's forge-account in the assignments mirror. +func (r *teamRig) bind(node, module string) { + r.bus.set(provider.AssignmentsBucket, node+"."+module, []byte(fmt.Sprintf( + `{"node":%q,"module":%q,"provisioning":{"provisions":[{"provision":"forge-account","provider":"novox","provider-module":"gitea","as":"mesh_%s"}]}}`, + node, module, strings.ReplaceAll(node+"_"+module, "-", "_")))) +} + +func (r *teamRig) unbind(node, module string) { + r.bus.mu.Lock() + delete(r.bus.buckets[provider.AssignmentsBucket], node+"."+module) + r.bus.mu.Unlock() +} + +// key reports a key in module's state keys, of the kind and for the account given. +func (r *teamRig) key(module, node, account, kind, public string) { + b, _ := json.Marshal(map[string]string{"node": node, "account": account, "kind": kind, "public": public, + "fingerprint": "SHA256:x", "at": "2026-10-20T10:00:00Z"}) + r.svc.SourceKeyChanged(module, node+"."+account, b) +} + +const ( + pubGo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + pubNode = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN" + pubOps = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" +) + +// 1481/AC1: an entry from mesh-build-go on ace: the forge user nox-builder and the team builders are ensured, the team's +// units are code, read only, on every repository, and the entry's values name nox-builder. The key is registered +// read-only under its title. +func TestABuildModulesEntryIsServedAsNoxBuilderInAReadOnlyTeam(t *testing.T) { + r := newTeamRig(t) + r.bind("ace", "mesh-build-go") + r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)") + r.pass() + + if !r.forge.users["nox-builder"] || r.forge.users["nox-agent"] { + t.Errorf("users = %v, want nox-builder alone", r.forge.users) + } + shape := r.forge.teams["builders"] + if shape == nil { + t.Fatalf("no team builders: %v", r.forge.teams) + } + units, _ := json.Marshal(shape["units_map"]) + if shape["permission"] != "read" || shape["includes_all_repositories"] != true || shape["can_create_org_repo"] != false || string(units) != `{"repo.code":"read"}` { + t.Errorf("the team's shape = %v (units %s), want code read on every repository and no other right", shape, units) + } + if got := strings.Join(r.forge.members["builders"], ","); got != "nox-builder" || len(r.forge.members["agents"]) != 0 { + t.Errorf("members = %v", r.forge.members) + } + keys := r.forge.keys["nox-builder"] + if len(keys) != 1 || keys[0].Title != "mesh:ace.mesh-build-go" || !keys[0].ReadOnly { + t.Errorf("nox-builder's keys = %+v, want one read-only key titled mesh:ace.mesh-build-go", keys) + } + e := r.bus.entry(t, "ace.mesh-build-go.forge-account") + if e.Values["user"] != "nox-builder" || e.Values["ssh_alias"] != "mesh-forge" || e.Values["host_key"] != "ssh-ed25519 AAAAC3HOSTKEY" { + t.Errorf("values = %v", e.Values) + } +} + +// 1481/AC2: an entry from ssh-client is what it was: nox-agent, the agents team, a key that may write. (The existing +// tests of the provider pass unchanged.) +func TestAnSshClientEntryIsStillTheAgentsWithAKeyThatMayWrite(t *testing.T) { + r := newTeamRig(t) + // the agents team exists, as the forge has it + r.forge.ids["agents"], r.forge.teams["agents"] = 5, map[string]any{} + r.bind("g14", "ssh-client") + r.key("ssh-client", "g14", "nox", "machine", pubA+" nox@g14 (mesh)") + r.pass() + if e := r.bus.entry(t, "g14.ssh-client.forge-account"); e.Values["user"] != "nox-agent" { + t.Errorf("values = %v", e.Values) + } + keys := r.forge.keys["nox-agent"] + if len(keys) != 1 || keys[0].Title != "mesh:g14.nox" || keys[0].ReadOnly { + t.Errorf("nox-agent's keys = %+v, want one key that may write", keys) + } + if r.forge.teams["builders"] != nil || r.forge.users["nox-builder"] { + t.Errorf("a machine's entry made the builders' account or team") + } + if got := strings.Join(r.forge.members["agents"], ","); got != "nox-agent" { + t.Errorf("agents = %v", r.forge.members) + } +} + +// 1481/AC3: a builder entry gone: only the keys titled mesh:.mesh-build-go are removed; the user and the team +// stay, and so do the other build module's key on that machine and a key of the operator's. +func TestABuilderEntryGoneRemovesItsOwnKeyAndNothingElse(t *testing.T) { + r := newTeamRig(t) + r.bind("ace", "mesh-build-go") + r.bind("ace", "mesh-build-node") + r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)") + r.key("mesh-build-node", "ace", "mesh-build-node", "builder", pubNode+" mesh-build-node@ace (mesh)") + r.pass() + r.forge.mu.Lock() + r.forge.keys["nox-builder"] = append(r.forge.keys["nox-builder"], forgeKey{ID: 99, Title: "my own", Key: pubOps}) + r.forge.mu.Unlock() + if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go,mesh:ace.mesh-build-node,my own" { + t.Fatalf("before: %s", got) + } + r.unbind("ace", "mesh-build-go") + r.pass() + if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-node,my own" { + t.Errorf("after: %s, want only the go module's key gone", got) + } + if !r.forge.users["nox-builder"] || r.forge.teams["builders"] == nil || len(r.forge.members["builders"]) != 1 { + t.Errorf("the user or the team went: users %v, members %v", r.forge.users, r.forge.members) + } + if r.bus.has("ace.mesh-build-go.forge-account") { + t.Errorf("the entry stayed") + } +} + +// A build module and ssh-client on one machine each have their own entry, keys, user and team. +func TestABuildModuleAndSshClientOnOneMachineDoNotShareAnything(t *testing.T) { + r := newTeamRig(t) + r.forge.ids["agents"], r.forge.teams["agents"] = 5, map[string]any{} + r.bind("ace", "ssh-client") + r.bind("ace", "mesh-build-go") + r.key("ssh-client", "ace", "nox", "machine", pubA+" nox@ace (mesh)") + r.key("mesh-build-go", "ace", "mesh-build-go", "builder", pubGo+" mesh-build-go@ace (mesh)") + r.pass() + if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "mesh:ace.nox" { + t.Errorf("nox-agent: %s", got) + } + if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go" { + t.Errorf("nox-builder: %s", got) + } + // ssh-client's entry gone: nox-builder's key stays. + r.unbind("ace", "ssh-client") + r.pass() + if got := strings.Join(r.forge.titles("nox-builder"), ","); got != "mesh:ace.mesh-build-go" { + t.Errorf("nox-builder after ssh-client left: %s", got) + } + if got := strings.Join(r.forge.titles("nox-agent"), ","); got != "" { + t.Errorf("nox-agent after ssh-client left: %s", got) + } +} + +// A build module's entry waits for its own module's builder key: a machine key of the same node, a key of the wrong +// kind in the build module's state, or the other build module's key is none. +func TestABuildModulesEntryWaitsForItsOwnBuilderKey(t *testing.T) { + r := newTeamRig(t) + r.bind("ace", "mesh-build-go") + r.key("ssh-client", "ace", "nox", "machine", pubA) // a machine's key + r.key("mesh-build-go", "ace", "mesh-build-go", "machine", pubGo) // the wrong kind in the build module's state + r.key("mesh-build-node", "ace", "mesh-build-node", "builder", pubNode) // the other build module's + out := r.pass() + if r.bus.has("ace.mesh-build-go.forge-account") || len(r.forge.users) != 0 || len(r.forge.teams) != 0 { + t.Fatalf("something was made without a builder key: users %v teams %v", r.forge.users, r.forge.teams) + } + if len(out) != 1 || !strings.Contains(out[0].Why, "mesh-build-go") || !strings.Contains(out[0].Why, "builder key") { + t.Errorf("the wait should name the module and the key: %+v", out) + } +} + +func TestTheKindOfAModulesKeyAndItsForgeUser(t *testing.T) { + for module, want := range map[string]string{"mesh-build-go": "nox-builder", "mesh-build-node": "nox-builder", "ssh-client": "nox-agent", "anything-else": "nox-agent"} { + got, err := (&Service{}).valuesForUser("ace", module) + if err != nil || got != want { + t.Errorf("%s: %q, %v; want %q", module, got, err, want) + } + } +} diff --git a/modules/mesh-build-go/files/forge b/modules/mesh-build-go/files/forge new file mode 100644 index 00000000..6bbf4e20 --- /dev/null +++ b/modules/mesh-build-go/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/modules/mesh-build-go/files/gitconfig b/modules/mesh-build-go/files/gitconfig new file mode 100644 index 00000000..05252302 --- /dev/null +++ b/modules/mesh-build-go/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/modules/mesh-build-go/files/known_hosts b/modules/mesh-build-go/files/known_hosts new file mode 100644 index 00000000..e0df6d81 --- /dev/null +++ b/modules/mesh-build-go/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod new file mode 100644 index 00000000..25a34806 --- /dev/null +++ b/modules/mesh-build-go/go.mod @@ -0,0 +1,21 @@ +module meshbuildgo + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020203945-1572e50e0ef0 + github.com/nats-io/nats-server/v2 v2.11.17 +) + +require ( + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/klauspost/compress v1.20.0 // indirect + github.com/minio/highwayhash v1.0.4 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/time v0.15.0 // indirect +) diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum new file mode 100644 index 00000000..37931482 --- /dev/null +++ b/modules/mesh-build-go/go.sum @@ -0,0 +1,29 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020200715-9e296295c05e h1:uf2o2A6dBOrISDUVw6U7+fv8ylwwF+UfRrGWKIQMFOE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020200715-9e296295c05e/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020203945-1572e50e0ef0 h1:mr9HyARwzeZT7FxFHv1Xc2z7QErvDZ/F/pBMIpjGSd0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020203945-1572e50e0ef0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json new file mode 100644 index 00000000..ea4a5a01 --- /dev/null +++ b/modules/mesh-build-go/module.json @@ -0,0 +1,142 @@ +{ + "module": "mesh-build-go", + "version": "1", + "runs-as": "mesh-build-go", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "state": [ + "toolchains", + "keys", + "steps" + ], + "tools": [ + "build_go_fetch", + "build_go_test", + "build_go_digest", + "build_go_build", + "build_go_status", + "build_go_health" + ], + "requires": [ + "forge-account" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-build-go", + "runtime-health": { + "kind": "tool", + "tool": "build_go_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-build-go", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-build-go" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-build-go", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-build-go-work", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "path": "/var/lib/mesh-build-go-cache", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-dir", + "type": "directory", + "path": "${dir:state}/.ssh", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-config", + "type": "file", + "path": "${dir:ssh-dir}/config", + "owner": "mesh-build-go", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${dir:ssh-dir}/known_hosts", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/known_hosts" + }, + { + "id": "git-config", + "type": "file", + "path": "${dir:state}/.config/git/config", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/gitconfig" + }, + { + "id": "go", + "type": "package", + "package": "go" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "openssh", + "type": "package", + "package": "openssh" + }, + { + "id": "postgresql", + "type": "package", + "package": "postgresql" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-build-go", + "binary": "mesh-build-go", + "loads": [ + "mesh-build-go" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +}