Monorepo for Tangled
tangled.org
1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7
8 "github.com/bluesky-social/indigo/xrpc"
9 "github.com/urfave/cli/v3"
10 "tangled.org/core/api/tangled"
11 "tangled.org/core/hook"
12 "tangled.org/core/idresolver"
13 "tangled.org/core/jetstream"
14 "tangled.org/core/knotserver/config"
15 knotdb "tangled.org/core/knotserver/db"
16 "tangled.org/core/knotserver/sandbox"
17 "tangled.org/core/log"
18 "tangled.org/core/notifier"
19 "tangled.org/core/rbac"
20)
21
22func Command() *cli.Command {
23 return &cli.Command{
24 Name: "server",
25 Usage: "run a knot server",
26 Action: Run,
27 Flags: []cli.Flag{
28 &cli.BoolFlag{
29 Name: "secure-mode",
30 Usage: "isolate git subprocesses to their own repository directory",
31 },
32 },
33 Description: `
34 Environment variables:
35 KNOT_SERVER_LISTEN_ADDR (default: 0.0.0.0:5555)
36 KNOT_SERVER_INTERNAL_LISTEN_ADDR (default: 127.0.0.1:5444)
37 KNOT_SERVER_DB_PATH (default: knotserver.db)
38 KNOT_SERVER_HOSTNAME (required)
39 KNOT_SERVER_JETSTREAM_ENDPOINT (default: wss://jetstream1.us-west.bsky.network/subscribe)
40 KNOT_SERVER_OWNER (required)
41 KNOT_SERVER_LOG_DIDS (default: true)
42 KNOT_SERVER_DEV (default: false)
43 KNOT_SERVER_SECURE_MODE (default: false)
44 KNOT_REPO_SCAN_PATH (default: /home/git)
45 KNOT_REPO_README (comma-separated list)
46 KNOT_REPO_MAIN_BRANCH (default: main)
47 KNOT_GIT_USER_NAME (default: Tangled)
48 KNOT_GIT_USER_EMAIL (default: noreply@tangled.sh)
49 APPVIEW_ENDPOINT (default: https://tangled.sh)
50 `,
51 }
52}
53
54func Run(ctx context.Context, cmd *cli.Command) error {
55 logger := log.FromContext(ctx)
56 logger = log.SubLogger(logger, cmd.Name)
57 ctx = log.IntoContext(ctx, logger)
58
59 c, err := config.Load(ctx)
60 if err != nil {
61 return fmt.Errorf("failed to load config: %w", err)
62 }
63
64 // CLI flag overrides env var.
65 if cmd.Bool("secure-mode") {
66 c.Server.SecureMode = true
67 }
68
69 if !c.Server.SecureMode {
70 err = hook.Setup(hook.Config(
71 hook.WithScanPath(c.Repo.ScanPath),
72 hook.WithInternalApi(c.Server.InternalListenAddr),
73 ))
74 if err != nil {
75 return fmt.Errorf("failed to setup hooks: %w", err)
76 }
77 logger.Info("successfully finished setting up hooks")
78 }
79
80 if c.Server.Dev {
81 logger.Info("running in dev mode, signature verification is disabled")
82 }
83
84 db, err := knotdb.Setup(ctx, c.Server.DBPath)
85 if err != nil {
86 return fmt.Errorf("failed to load db: %w", err)
87 }
88
89 e, err := rbac.NewEnforcer(c.Server.DBPath)
90 if err != nil {
91 return fmt.Errorf("failed to setup rbac enforcer: %w", err)
92 }
93
94 e.E.EnableAutoSave(true)
95
96 jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{
97 tangled.PublicKeyNSID,
98 tangled.RepoNSID,
99 tangled.RepoPullNSID,
100 }, nil, log.SubLogger(logger, "jetstream"), db, true, c.Server.LogDids)
101 if err != nil {
102 logger.Error("failed to setup jetstream", "error", err)
103 }
104
105 notifier := notifier.New()
106
107 resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
108
109 if err := BackfillKnotMembers(ctx, db, e, c.Server.Owner, logger); err != nil {
110 logger.Warn("knot member backfill failed, continuing", "err", err)
111 }
112 if err := BackfillCollaborators(ctx, db, e, logger, false); err != nil {
113 logger.Warn("collaborator backfill failed, continuing", "err", err)
114 }
115
116 // probe and initialise the sandbox backend.
117 var sb sandbox.Backend
118 if c.Server.SecureMode {
119 var warn string
120 sb, warn = sandbox.New(func(repoPath string) (uint32, uint32, error) {
121 return sandbox.LookupUIDForRepoPath(c.Repo.ScanPath, repoPath)
122 })
123 if warn != "" {
124 return fmt.Errorf("secure-mode: %s", warn)
125 }
126 logger.Info("secure-mode: activated", "backend", sb.Name())
127
128 // refuse to start if any repos have not yet been isolation-migrated.
129 unmigrated, countErr := db.CountUnmigratedRepos()
130 if countErr != nil {
131 return fmt.Errorf("secure-mode: checking unmigrated repos: %w", countErr)
132 }
133 if unmigrated > 0 {
134 return fmt.Errorf(
135 "secure-mode: %d repo(s) have not been isolation-migrated; "+
136 "run 'knot migrate-isolation' first",
137 unmigrated,
138 )
139 }
140 } else {
141 sb = &sandbox.NoopBackend{}
142 }
143
144 go func() {
145 migrated := migrateReposOnStartup(ctx, c, db, e, ¬ifier, log.SubLogger(logger, "migrate"))
146 if err := BackfillCollaborators(ctx, db, e, logger, migrated); err != nil {
147 logger.Warn("collaborator backfill failed, continuing", "err", err)
148 }
149 }()
150
151 mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver, sb)
152 if err != nil {
153 return fmt.Errorf("failed to setup server: %w", err)
154 }
155
156 imux := Internal(ctx, c, db, e, ¬ifier, resolver)
157
158 logger.Info("starting internal server", "address", c.Server.InternalListenAddr)
159 go http.ListenAndServe(c.Server.InternalListenAddr, imux)
160
161 // TODO(boltless): too lazy here. should clear this up
162 go func() {
163 input := &tangled.SyncRequestCrawl_Input{
164 Hostname: c.Server.Hostname,
165 }
166 for _, knotmirror := range c.KnotMirrors {
167 xrpcc := xrpc.Client{Host: knotmirror}
168 if err := tangled.SyncRequestCrawl(ctx, &xrpcc, input); err != nil {
169 logger.Error("error requesting crawl", "err", err)
170 } else {
171 logger.Info("crawl requested successfully")
172 }
173 }
174 }()
175
176 logger.Info("starting main server", "address", c.Server.ListenAddr)
177 logger.Error("server error", "error", http.ListenAndServe(c.Server.ListenAddr, mux))
178
179 return nil
180}