This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

8.4 kB 306 lines
1// similar to zoekt-dynamic-indexserver, but targetting Tangled repos 2package main 3 4import ( 5 "bytes" 6 "context" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "log" 11 "net" 12 "net/http" 13 "os" 14 "os/exec" 15 "os/signal" 16 "strconv" 17 "syscall" 18 "time" 19 20 "github.com/bluesky-social/indigo/atproto/identity" 21 "github.com/bluesky-social/indigo/atproto/syntax" 22 "github.com/carlmjohnson/versioninfo" 23 "github.com/sourcegraph/zoekt" 24 "github.com/sourcegraph/zoekt/gitindex" 25 "github.com/sourcegraph/zoekt/index" 26 "github.com/urfave/cli/v3" 27) 28 29func loggedRun(cmd *exec.Cmd) error { 30 outBuf := &bytes.Buffer{} 31 errBuf := &bytes.Buffer{} 32 cmd.Stdout = outBuf 33 cmd.Stderr = errBuf 34 35 log.Printf("run %v", cmd.Args) 36 if err := cmd.Run(); err != nil { 37 log.Printf("command %s failed: %v\nOUT: %s\nERR: %s", 38 cmd.Args, err, outBuf.String(), errBuf.String()) 39 return fmt.Errorf("command %s failed: %v", cmd.Args, err) 40 } 41 42 return nil 43} 44 45// This function is declared as var so that we can stub it in test 46var executeCmd = func(ctx context.Context, name string, arg ...string) error { 47 cmd := exec.CommandContext(ctx, name, arg...) 48 cmd.Stdin = &bytes.Buffer{} 49 err := loggedRun(cmd) 50 51 return err 52} 53 54func main() { 55 if err := run(os.Args); err != nil { 56 log.Fatal(err) 57 } 58} 59 60func run(args []string) error { 61 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 62 defer cancel() 63 64 app := cli.Command{ 65 Name: "zoekt-tngl-indexserver", 66 Usage: "tangled zoekt index server", 67 Version: versioninfo.Short(), 68 } 69 app.Flags = []cli.Flag{ 70 &cli.StringFlag{ 71 Name: "index-dir", 72 Usage: "directory holding index shards.", 73 Required: true, 74 Sources: cli.EnvVars("TANGLED_ZOEKT_INDEX_DIR"), 75 }, 76 &cli.StringFlag{ 77 Name: "appview-url", 78 Usage: "appview url. used when debugging", 79 Value: "https://tangled.org", 80 Sources: cli.EnvVars("TANGLED_ZOEKT_APPVIEW_URL"), 81 }, 82 } 83 app.Commands = []*cli.Command{ 84 { 85 Name: "serve", 86 Usage: "run the index server daemon", 87 Action: runIndexServer, 88 Flags: []cli.Flag{ 89 &cli.StringFlag{ 90 Name: "plc-url", 91 Usage: "atproto PLC directory.", 92 Value: "https://plc.directory", 93 Sources: cli.EnvVars("TANGLED_ZOEKT_PLC_URL", "TANGLED_PLC_URL", "ATP_PLC_HOST"), 94 }, 95 &cli.DurationFlag{ 96 Name: "index-timeout", 97 Usage: "kill index job after this much time.", 98 Value: time.Hour, 99 Sources: cli.EnvVars("TANGLED_ZOEKT_INDEX_TIMEOUT"), 100 }, 101 &cli.IntFlag{ 102 Name: "index-concurrency", 103 Usage: "number of repositories to index concurrently.", 104 Value: 4, 105 Sources: cli.EnvVars("TANGLED_ZOEKT_INDEX_CONCURRENCY"), 106 }, 107 &cli.IntFlag{ 108 Name: "index-queue-size", 109 Usage: "max index queue size", 110 Value: 100, 111 Sources: cli.EnvVars("TANGLED_ZOEKT_INDEX_QUEUE_SIZE"), 112 }, 113 &cli.StringFlag{ 114 Name: "listen", 115 Usage: "listen on this address", 116 Value: ":6060", 117 Sources: cli.EnvVars("TANGLED_ZOEKT_SERVER_LISTEN"), 118 }, 119 }, 120 }, 121 { 122 Name: "index", 123 Usage: "manually index a git repository", 124 Action: runIndex, 125 Arguments: []cli.Argument{ 126 &cli.StringArg{ 127 Name: "git-dir", 128 UsageText: "path to fetched git repository.", 129 }, 130 &cli.StringArg{ 131 Name: "repo", 132 UsageText: "json-encoded repository information", 133 }, 134 }, 135 }, 136 } 137 138 return app.Run(ctx, args) 139} 140 141type Config struct { 142 // IndexDir is the index directory to use. 143 IndexDir string 144 145 IndexTimeout time.Duration 146 147 // IndexConcurrency is the number of repositories we index at once. 148 IndexConcurrency int 149 IndexQueueSize int 150 151 PlcUrl string 152 AppviewUrl string 153 Listen string 154} 155 156func createMissingDirectories(cfg *Config) { 157 for _, s := range []string{cfg.IndexDir} { 158 if err := os.MkdirAll(s, 0o755); err != nil { 159 log.Fatalf("MkdirAll %s: %v", s, err) 160 } 161 } 162} 163 164type Repo struct { 165 Did syntax.DID // repo DID 166 Owner syntax.DID 167 Slug syntax.RecordKey 168 Knot string // knot service url derived from #atproto_pds service endpoint 169 Branches []zoekt.RepositoryBranch 170} 171 172func (r *Repo) CloneURL() string { 173 return r.Knot + "/" + r.Did.String() 174} 175 176func runIndexServer(ctx context.Context, cmd *cli.Command) error { 177 cfg := &Config{ 178 IndexDir: cmd.String("index-dir"), 179 IndexTimeout: cmd.Duration("index-timeout"), 180 IndexConcurrency: cmd.Int("index-concurrency"), 181 IndexQueueSize: cmd.Int("index-queue-size"), 182 PlcUrl: cmd.String("plc-url"), 183 AppviewUrl: cmd.String("appview-url"), 184 Listen: cmd.String("listen"), 185 } 186 createMissingDirectories(cfg) 187 188 server := NewIndexServer(cfg) 189 go server.Run(ctx) 190 191 <-ctx.Done() 192 return ctx.Err() 193} 194 195// sub-process to index a fetched repository 196func runIndex(ctx context.Context, cmd *cli.Command) error { 197 var ( 198 indexDir = cmd.String("index-dir") 199 appviewUrl = cmd.String("appview-url") 200 ) 201 202 gitDir := cmd.StringArg("git-dir") 203 if gitDir == "" { 204 return errors.New("git-dir is required.") 205 } 206 207 repoRaw := cmd.StringArg("repo") 208 if repoRaw == "" { 209 return errors.New("repo is required.") 210 } 211 212 var repo Repo 213 if err := json.Unmarshal([]byte(repoRaw), &repo); err != nil { 214 return fmt.Errorf("invalid repo: %w", err) 215 } 216 217 var branches []string 218 for _, b := range repo.Branches { 219 branches = append(branches, b.Name) 220 } 221 222 buildOpts := index.Options{} 223 buildOpts.SetDefaults() 224 225 buildOpts.IndexDir = indexDir 226 227 buildOpts.ShardPrefixOverride = repo.Did.String() 228 229 // Tangled templates 230 webUrl := fmt.Sprintf("%s/%s", appviewUrl, repo.Did) 231 buildOpts.RepositoryDescription.CommitURLTemplate = fmt.Sprintf("%s/commit/{{.Version}}", webUrl) 232 buildOpts.RepositoryDescription.FileURLTemplate = fmt.Sprintf("%s/blob/{{.Version}}/{{.Path}}", webUrl) 233 buildOpts.RepositoryDescription.LineFragmentTemplate = "#L{{.LineNumber}}" 234 235 buildOpts.RepositoryDescription.Name = repo.Slug.String() 236 buildOpts.RepositoryDescription.URL = webUrl 237 buildOpts.RepositoryDescription.Metadata = map[string]string{ 238 "foo": "bar", // for testing 239 "did": repo.Did.String(), 240 "owner": repo.Owner.String(), 241 "knot": repo.Knot, 242 } 243 // buildOpts.RepositoryDescription.Source = gitDir // configured later in IndexGitRepo 244 buildOpts.RepositoryDescription.Branches = nil 245 buildOpts.RepositoryDescription.SubRepoMap = nil 246 buildOpts.RepositoryDescription.RawConfig = map[string]string{ 247 "priority": strconv.FormatFloat(0.0, 'g', -1, 64), 248 "public": marshalBool(true), 249 "fork": marshalBool(false), 250 // Calculate repo rank based on the latest commit date. 251 "latestCommitDate": marshalBool(true), 252 } 253 buildOpts.RepositoryDescription.Rank = 0 254 // buildOpts.RepositoryDescription.IndexOptions = "" // configured later in IndexGitRepo 255 // buildOpts.RepositoryDescription.LatestCommitDate = _ // configured later in IndexGitRepo 256 buildOpts.RepositoryDescription.FileTombstones = nil 257 258 gitOpts := gitindex.Options{ 259 RepoDir: gitDir, 260 Submodules: false, 261 Incremental: true, 262 AllowMissingBranch: false, 263 RepoCacheDir: "", 264 BuildOptions: buildOpts, 265 BranchPrefix: "refs/heads/", 266 Branches: branches, 267 DeltaShardNumberFallbackThreshold: 0, 268 } 269 if _, err := gitindex.IndexGitRepo(gitOpts); err != nil { 270 return err 271 } 272 return nil 273} 274 275func marshalBool(b bool) string { 276 if b { 277 return "1" 278 } 279 return "0" 280} 281 282func baseDir(plc string) identity.Directory { 283 base := identity.BaseDirectory{ 284 PLCURL: plc, 285 HTTPClient: http.Client{ 286 Timeout: time.Second * 10, 287 Transport: &http.Transport{ 288 Proxy: http.ProxyFromEnvironment, 289 // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad. 290 IdleConnTimeout: time.Millisecond * 1000, 291 MaxIdleConns: 100, 292 }, 293 }, 294 Resolver: net.Resolver{ 295 Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 296 d := net.Dialer{Timeout: time.Second * 3} 297 return d.DialContext(ctx, network, address) 298 }, 299 }, 300 TryAuthoritativeDNS: true, 301 // primary Bluesky PDS instance only supports HTTP resolution method 302 SkipDNSDomainSuffixes: []string{".bsky.social"}, 303 UserAgent: "indigo-identity/" + versioninfo.Short(), 304 } 305 return identity.NewCacheDirectory(&base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) 306}