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 }, nil, log.SubLogger(logger, "jetstream"), db, true, c.Server.LogDids)
100 if err != nil {
101 logger.Error("failed to setup jetstream", "error", err)
102 }
103
104 notifier := notifier.New()
105
106 resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
107
108 if err := BackfillKnotMembers(ctx, db, e, c.Server.Owner, logger); err != nil {
109 logger.Warn("knot member backfill failed, continuing", "err", err)
110 }
111 if err := BackfillCollaborators(ctx, db, e, logger, false); err != nil {
112 logger.Warn("collaborator backfill failed, continuing", "err", err)
113 }
114
115 // probe and initialise the sandbox backend.
116 var sb sandbox.Backend
117 if c.Server.SecureMode {
118 var warn string
119 sb, warn = sandbox.New(func(repoPath string) (uint32, uint32, error) {
120 return sandbox.LookupUIDForRepoPath(c.Repo.ScanPath, repoPath)
121 })
122 if warn != "" {
123 return fmt.Errorf("secure-mode: %s", warn)
124 }
125 logger.Info("secure-mode: activated", "backend", sb.Name())
126
127 // refuse to start if any repos have not yet been isolation-migrated.
128 unmigrated, countErr := db.CountUnmigratedRepos()
129 if countErr != nil {
130 return fmt.Errorf("secure-mode: checking unmigrated repos: %w", countErr)
131 }
132 if unmigrated > 0 {
133 return fmt.Errorf(
134 "secure-mode: %d repo(s) have not been isolation-migrated; "+
135 "run 'knot migrate-isolation' first",
136 unmigrated,
137 )
138 }
139 } else {
140 sb = &sandbox.NoopBackend{}
141 }
142
143 go func() {
144 migrated := migrateReposOnStartup(ctx, c, db, e, ¬ifier, log.SubLogger(logger, "migrate"))
145 if err := BackfillCollaborators(ctx, db, e, logger, migrated); err != nil {
146 logger.Warn("collaborator backfill failed, continuing", "err", err)
147 }
148 }()
149
150 mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver, sb)
151 if err != nil {
152 return fmt.Errorf("failed to setup server: %w", err)
153 }
154
155 imux := Internal(ctx, c, db, e, ¬ifier, resolver)
156
157 logger.Info("starting internal server", "address", c.Server.InternalListenAddr)
158 go http.ListenAndServe(c.Server.InternalListenAddr, imux)
159
160 // TODO(boltless): too lazy here. should clear this up
161 go func() {
162 input := &tangled.SyncRequestCrawl_Input{
163 Hostname: c.Server.Hostname,
164 }
165 for _, knotmirror := range c.KnotMirrors {
166 xrpcc := xrpc.Client{Host: knotmirror}
167 if err := tangled.SyncRequestCrawl(ctx, &xrpcc, input); err != nil {
168 logger.Error("error requesting crawl", "err", err)
169 } else {
170 logger.Info("crawl requested successfully")
171 }
172 }
173 }()
174
175 logger.Info("starting main server", "address", c.Server.ListenAddr)
176 logger.Error("server error", "error", http.ListenAndServe(c.Server.ListenAddr, mux))
177
178 return nil
179}