// Copyright (C) 2023-2024 Umorpha Systems // SPDX-License-Identifier: AGPL-3.0-or-later package main import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "io" "math/big" "net/http" "net/url" "os" "time" "github.com/datawire/ocibuild/pkg/cliutil" "github.com/spf13/cobra" source "git.mothstuff.lol/lukeshu/eclipse" "git.mothstuff.lol/lukeshu/eclipse/lib/common" "git.mothstuff.lol/lukeshu/eclipse/lib/follower" ) func main() { argparser := &cobra.Command{ Use: os.Args[0] + " [flags] JOBS_URL", Short: "Choose a job from the leader server", Args: cliutil.WrapPositionalArgs(cobra.NoArgs), SilenceErrors: true, // we'll handle this ourselves after .ExecuteContext() SilenceUsage: true, // FlagErrorFunc will handle this CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: true, }, } argparser.SetFlagErrorFunc(cliutil.FlagErrorFunc) argparser.SetHelpTemplate(cliutil.HelpTemplate) var cfgFile string argparser.Flags().StringVar(&cfgFile, "config", source.DefaultFollowerConfigFile, "Config file to use") _ = argparser.MarkFlagFilename("config", "yml", "yaml") argparser.RunE = func(*cobra.Command, []string) error { return Pick(cfgFile) } ctx := context.Background() if err := argparser.ExecuteContext(ctx); err != nil { fmt.Fprintf(os.Stderr, "%v: error: %v\n", argparser.CommandPath(), err) os.Exit(1) } } func randInt(max int) (int, error) { bigN, err := rand.Int(rand.Reader, big.NewInt(int64(max))) if err != nil { return 0, err } return int(bigN.Int64()), nil } func Pick(cfgFile string) error { cfg, err := follower.LoadConfig(cfgFile) if err != nil { return err } u, err := url.Parse(cfg.LeaderURL) if err != nil { return err } u.Path = "/jobs/" u.RawQuery = url.Values{ "status": []string{"new"}, "wait": []string{"true"}, }.Encode() httpClient := &http.Client{ Timeout: 45 * time.Second, } // TODO: If we go 1 minute without an "HTTP 100 Continue" // keepalive, then fail with a timeout. resp, err := httpClient.Get(u.String()) if err != nil { return err } if resp.StatusCode != http.StatusOK { err := fmt.Errorf("HTTP %s", resp.Status) body, _ := io.ReadAll(resp.Body) if len(body) > 0 { err = fmt.Errorf("%w\n%s", err, body) } return err } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } var options []common.Job if err := json.Unmarshal(bodyBytes, &options); err != nil { return err } if len(options) == 0 { return errors.New("no jobs to choose from; this indicates a server bug, it should never return an empty non-error response") } n, err := randInt(len(options)) if err != nil { return err } u.Path = fmt.Sprintf("/jobs/%d/", options[n].ID) u.RawQuery = "" fmt.Println(u) return nil }