diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index 4ca12503..744ce539 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -340,3 +340,111 @@ func (a *APIController) CreateRepoPoolHandler(w http.ResponseWriter, r *http.Req w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(pool) } + +func (a *APIController) ListRepoPoolsHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + repoID, ok := vars["repoID"] + if !ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No repo ID specified", + }) + return + } + + pools, err := a.r.ListRepoPools(ctx, repoID) + if err != nil { + log.Printf("listing pools: %+v", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pools) +} + +func (a *APIController) GetRepoPoolHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + repoID, repoOk := vars["repoID"] + poolID, poolOk := vars["poolID"] + if !repoOk || !poolOk { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No repo or pool ID specified", + }) + return + } + + pool, err := a.r.GetRepoPoolByID(ctx, repoID, poolID) + if err != nil { + log.Printf("listing pools: %+v", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pool) +} + +func (a *APIController) DeleteRepoPoolHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + repoID, repoOk := vars["repoID"] + poolID, poolOk := vars["poolID"] + if !repoOk || !poolOk { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No repo or pool ID specified", + }) + return + } + + if err := a.r.DeleteRepoPool(ctx, repoID, poolID); err != nil { + log.Printf("removing pool: %+v", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + +} + +func (a *APIController) UpdateRepoPoolHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + vars := mux.Vars(r) + repoID, repoOk := vars["repoID"] + poolID, poolOk := vars["poolID"] + if !repoOk || !poolOk { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Bad Request", + Details: "No repo or pool ID specified", + }) + return + } + + var poolData runnerParams.UpdatePoolParams + if err := json.NewDecoder(r.Body).Decode(&poolData); err != nil { + log.Printf("failed to decode: %+v", err) + handleError(w, gErrors.ErrBadRequest) + return + } + + pool, err := a.r.UpdateRepoPool(ctx, repoID, poolID, poolData) + if err != nil { + log.Printf("error creating repository pool: %+v", err) + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(pool) +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 5a3422db..b63310c6 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -41,12 +41,17 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl // Repos and pools // ///////////////////// // Get pool - apiRouter.Handle("/repositories/{repoID}/pools/{poolID:poolID\\/?}", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("GET", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.GetRepoPoolHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.GetRepoPoolHandler))).Methods("GET", "OPTIONS") // Delete pool - apiRouter.Handle("/repositories/{repoID}/pools/{poolID:poolID\\/?}", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.DeleteRepoPoolHandler))).Methods("DELETE", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.DeleteRepoPoolHandler))).Methods("DELETE", "OPTIONS") + // Update pool + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(os.Stdout, http.HandlerFunc(han.UpdateRepoPoolHandler))).Methods("PUT", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(os.Stdout, http.HandlerFunc(han.UpdateRepoPoolHandler))).Methods("PUT", "OPTIONS") // List pools - apiRouter.Handle("/repositories/{repoID}/pools/", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("GET", "OPTIONS") - apiRouter.Handle("/repositories/{repoID}/pools", log(os.Stdout, http.HandlerFunc(han.CatchAll))).Methods("GET", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools/", log(os.Stdout, http.HandlerFunc(han.ListRepoPoolsHandler))).Methods("GET", "OPTIONS") + apiRouter.Handle("/repositories/{repoID}/pools", log(os.Stdout, http.HandlerFunc(han.ListRepoPoolsHandler))).Methods("GET", "OPTIONS") // Create pool apiRouter.Handle("/repositories/{repoID}/pools/", log(os.Stdout, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS") apiRouter.Handle("/repositories/{repoID}/pools", log(os.Stdout, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS") diff --git a/auth/auth.go b/auth/auth.go index ae4b6cc0..3665fd1b 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -28,16 +28,11 @@ type Authenticator struct { } func (a *Authenticator) IsInitialized() bool { - info, err := a.store.ControllerInfo() - if err != nil { - return false + if a.store.HasAdminUser(context.Background()) { + return true } - if info.ControllerID.String() == "" { - return false - } - - return true + return false } func (a *Authenticator) GetJWTToken(ctx context.Context) (string, error) { @@ -105,9 +100,6 @@ func (a *Authenticator) InitController(ctx context.Context, param params.NewUser param.Password = hashed - if _, err := a.store.InitController(); err != nil { - return params.User{}, errors.Wrap(err, "initializing controller") - } return a.store.CreateUser(ctx, param) } diff --git a/cmd/run-cli/LICENSE b/cmd/run-cli/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/cmd/run-cli/client/client.go b/cmd/run-cli/client/client.go new file mode 100644 index 00000000..075b7843 --- /dev/null +++ b/cmd/run-cli/client/client.go @@ -0,0 +1,85 @@ +package client + +import ( + "encoding/json" + "fmt" + + apiParams "runner-manager/apiserver/params" + "runner-manager/cmd/run-cli/config" + "runner-manager/params" + + "github.com/go-resty/resty/v2" + "github.com/pkg/errors" +) + +func NewClient(name string, cfg config.Manager) *Client { + cli := resty.New() + return &Client{ + ManagerName: name, + Config: cfg, + client: cli, + } +} + +type Client struct { + ManagerName string + Config config.Manager + client *resty.Client +} + +func (c *Client) decodeAPIError(body []byte) (apiParams.APIErrorResponse, error) { + var errDetails apiParams.APIErrorResponse + if err := json.Unmarshal(body, &errDetails); err != nil { + return apiParams.APIErrorResponse{}, errors.Wrap(err, "decoding response") + } + + return errDetails, fmt.Errorf("error in API call: %s", errDetails.Details) +} + +func (c *Client) InitManager(url string, param params.NewUserParams) (params.User, error) { + body, err := json.Marshal(param) + if err != nil { + return params.User{}, errors.Wrap(err, "marshaling body") + } + url = fmt.Sprintf("%s/api/v1/first-run/", url) + + var response params.User + resp, err := c.client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + SetResult(&response). + Post(url) + if err != nil || resp.IsError() { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return params.User{}, errors.Wrap(decErr, "sending request") + } + return params.User{}, fmt.Errorf("error running init: %s", apiErr.Details) + } + + return response, nil +} + +func (c *Client) Login(url string, param params.PasswordLoginParams) (string, error) { + body, err := json.Marshal(param) + if err != nil { + return "", errors.Wrap(err, "marshaling body") + } + url = fmt.Sprintf("%s/api/v1/auth/login", url) + + var response params.JWTResponse + resp, err := c.client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + SetResult(&response). + Post(url) + if err != nil { + apiErr, decErr := c.decodeAPIError(resp.Body()) + if decErr != nil { + return "", errors.Wrap(err, "sending request") + } + return "", fmt.Errorf("error running init: %s", apiErr.Details) + } + + return response.Token, nil +} diff --git a/cmd/run-cli/cmd/init.go b/cmd/run-cli/cmd/init.go new file mode 100644 index 00000000..957e0ebc --- /dev/null +++ b/cmd/run-cli/cmd/init.go @@ -0,0 +1,113 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "runner-manager/cmd/run-cli/config" + "runner-manager/params" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// initCmd represents the init command +var initCmd = &cobra.Command{ + Use: "init", + SilenceUsage: true, + Short: "Initialize a newly installed runner-manager", + Long: `Initiallize a new installation of runner-manager. + +A newly installed runner manager needs to be initialized to become +functional. This command sets the administrative user and password, +generates a controller UUID which is used internally to identify runners +created by the manager and enables the service. + +Example usage: + +run-cli login --name=dev --url=https://runner.example.com --username=admin --password=superSecretPassword +`, + RunE: func(cmd *cobra.Command, args []string) error { + if cfg != nil { + if cfg.HasManager(loginName) { + return fmt.Errorf("a manager with name %s already exists in your local config", loginName) + } + } + + newUser := params.NewUserParams{ + Username: loginUserName, + Password: loginPassword, + FullName: loginFullName, + Email: loginEmail, + } + response, err := cli.InitManager(loginURL, newUser) + if err != nil { + return errors.Wrap(err, "initializing manager") + } + + loginParams := params.PasswordLoginParams{ + Username: loginUserName, + Password: loginPassword, + } + + token, err := cli.Login(loginURL, loginParams) + if err != nil { + return errors.Wrap(err, "authenticating") + } + + if cfg == nil { + // we're creating a new config + cfg = &config.Config{ + Managers: []config.Manager{}, + } + } + + cfg.Managers = append(cfg.Managers, config.Manager{ + Name: loginName, + BaseURL: loginURL, + Token: token, + }) + + cfg.ActiveManager = loginName + + if err := cfg.SaveConfig(); err != nil { + return errors.Wrap(err, "saving config") + } + + renderUserTable(response) + return nil + }, +} + +func init() { + rootCmd.AddCommand(initCmd) + + initCmd.Flags().StringVarP(&loginName, "name", "n", "", "A name for this runner manager") + initCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") + initCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "The desired administrative username") + initCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The admin password") + initCmd.Flags().StringVarP(&loginFullName, "full-name", "f", "", "Full name of the user") + initCmd.Flags().StringVarP(&loginEmail, "email", "e", "", "Email address") + initCmd.MarkFlagRequired("name") + initCmd.MarkFlagRequired("url") + initCmd.MarkFlagRequired("username") + initCmd.MarkFlagRequired("password") + initCmd.MarkFlagRequired("full-name") + initCmd.MarkFlagRequired("email") +} + +func renderUserTable(user params.User) { + t := table.NewWriter() + header := table.Row{"Field", "Value"} + t.AppendHeader(header) + + t.AppendRow(table.Row{"ID", user.ID}) + t.AppendRow(table.Row{"Username", user.Username}) + t.AppendRow(table.Row{"Email", user.Email}) + t.AppendRow(table.Row{"Enabled", user.Enabled}) + fmt.Println(t.Render()) +} diff --git a/cmd/run-cli/cmd/login.go b/cmd/run-cli/cmd/login.go new file mode 100644 index 00000000..b29031e4 --- /dev/null +++ b/cmd/run-cli/cmd/login.go @@ -0,0 +1,42 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + loginName string + loginURL string + loginPassword string + loginUserName string + loginFullName string + loginEmail string +) + +// loginCmd represents the login command +var loginCmd = &cobra.Command{ + Use: "login", + SilenceUsage: true, + Short: "Log into a manager", + Long: `Performs login for this machine on a remote runner-manager: + +run-cli login --name=dev --url=https://runner.example.com`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("--> %v %v\n", cfg, configErr) + }, +} + +func init() { + rootCmd.AddCommand(loginCmd) + + loginCmd.Flags().StringVarP(&loginName, "name", "n", "", "A name for this runner manager") + loginCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") + loginCmd.MarkFlagRequired("name") + loginCmd.MarkFlagRequired("url") +} diff --git a/cmd/run-cli/cmd/organization.go b/cmd/run-cli/cmd/organization.go new file mode 100644 index 00000000..88031875 --- /dev/null +++ b/cmd/run-cli/cmd/organization.go @@ -0,0 +1,41 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// organizationCmd represents the organization command +var organizationCmd = &cobra.Command{ + Use: "organization", + SilenceUsage: true, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("organization called") + }, +} + +func init() { + rootCmd.AddCommand(organizationCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // organizationCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // organizationCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/run-cli/cmd/provider.go b/cmd/run-cli/cmd/provider.go new file mode 100644 index 00000000..36e53e69 --- /dev/null +++ b/cmd/run-cli/cmd/provider.go @@ -0,0 +1,41 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// providerCmd represents the provider command +var providerCmd = &cobra.Command{ + Use: "provider", + SilenceUsage: true, + Short: "Interacts with the providers API resource.", + Long: `Run operations on the provider resource. + +Currently this command only lists all available configured +providers. Providers are added to the configuration file of +the service and are referenced by name when adding repositories +and organizations. Runners will be created in these environments.`, + Run: nil, +} + +func init() { + providerCmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List all configured providers", + Long: `List all cloud providers configured with the service.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("provider list called") + return fmt.Errorf("I failed :(") + }, + }) + + rootCmd.AddCommand(providerCmd) +} diff --git a/cmd/run-cli/cmd/repository.go b/cmd/run-cli/cmd/repository.go new file mode 100644 index 00000000..7ad3d568 --- /dev/null +++ b/cmd/run-cli/cmd/repository.go @@ -0,0 +1,41 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// repositoryCmd represents the repository command +var repositoryCmd = &cobra.Command{ + Use: "repository", + SilenceUsage: true, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("repository called") + }, +} + +func init() { + rootCmd.AddCommand(repositoryCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // repositoryCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // repositoryCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/run-cli/cmd/root.go b/cmd/run-cli/cmd/root.go new file mode 100644 index 00000000..38c9026e --- /dev/null +++ b/cmd/run-cli/cmd/root.go @@ -0,0 +1,45 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "os" + "runner-manager/cmd/run-cli/client" + "runner-manager/cmd/run-cli/config" + + "github.com/spf13/cobra" +) + +var cfg *config.Config +var mgr config.Manager +var configErr error +var cli *client.Client +var active string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "run-cli", + Short: "Runner manager CLI app", + Long: `CLI for the github self hosted runners manager.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + cobra.OnInitialize(initConfig) + + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func initConfig() { + cfg, configErr = config.LoadConfig() + if configErr == nil { + mgr, _ = cfg.GetActiveConfig() + } + cli = client.NewClient(active, mgr) +} diff --git a/cmd/run-cli/cmd/runner.go b/cmd/run-cli/cmd/runner.go new file mode 100644 index 00000000..f92f082d --- /dev/null +++ b/cmd/run-cli/cmd/runner.go @@ -0,0 +1,41 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// runnerCmd represents the runner command +var runnerCmd = &cobra.Command{ + Use: "runner", + SilenceUsage: true, + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("runner called") + }, +} + +func init() { + rootCmd.AddCommand(runnerCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // runnerCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // runnerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/run-cli/config/config.go b/cmd/run-cli/config/config.go new file mode 100644 index 00000000..b8b5d372 --- /dev/null +++ b/cmd/run-cli/config/config.go @@ -0,0 +1,100 @@ +package config + +import ( + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/pkg/errors" + + runnerErrors "runner-manager/errors" +) + +const ( + DefaultAppFolder = "run-cli" + DefaultConfigFileName = "config.toml" +) + +func getConfigFilePath() (string, error) { + configDir, err := getHomeDir() + if err != nil { + return "", errors.Wrap(err, "fetching home folder") + } + + if err := ensureHomeDir(configDir); err != nil { + return "", errors.Wrap(err, "ensuring config dir") + } + + cfgFile := filepath.Join(configDir, DefaultConfigFileName) + return cfgFile, nil +} + +func LoadConfig() (*Config, error) { + cfgFile, err := getConfigFilePath() + if err != nil { + return nil, errors.Wrap(err, "fetching config") + } + + var config Config + if _, err := toml.DecodeFile(cfgFile, &config); err != nil { + return nil, errors.Wrap(err, "decoding toml") + } + + return &config, nil +} + +type Config struct { + Managers []Manager `toml:"manager"` + ActiveManager string `toml:"active_manager"` +} + +func (c *Config) HasManager(mgr string) bool { + if mgr == "" { + return false + } + for _, val := range c.Managers { + if val.Name == mgr { + return true + } + } + return false +} + +func (c *Config) GetActiveConfig() (Manager, error) { + if c.ActiveManager == "" { + return Manager{}, runnerErrors.ErrNotFound + } + + for _, val := range c.Managers { + if val.Name == c.ActiveManager { + return val, nil + } + } + return Manager{}, runnerErrors.ErrNotFound +} + +func (c *Config) SaveConfig() error { + cfgFile, err := getConfigFilePath() + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return errors.Wrap(err, "getting config") + } + } + cfgHandle, err := os.Create(cfgFile) + if err != nil { + errors.Wrap(err, "getting file handle") + } + + encoder := toml.NewEncoder(cfgHandle) + if err := encoder.Encode(c); err != nil { + return errors.Wrap(err, "saving config") + } + + return nil +} + +type Manager struct { + Name string `toml:"name"` + BaseURL string `toml:"base_url"` + Token string `toml:"bearer_token"` +} diff --git a/cmd/run-cli/config/home.go b/cmd/run-cli/config/home.go new file mode 100644 index 00000000..dc76b95f --- /dev/null +++ b/cmd/run-cli/config/home.go @@ -0,0 +1,21 @@ +package config + +import ( + "os" + + "github.com/pkg/errors" +) + +func ensureHomeDir(folder string) error { + if _, err := os.Stat(folder); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return errors.Wrap(err, "checking home dir") + } + + if err := os.MkdirAll(folder, 0o710); err != nil { + return errors.Wrapf(err, "creating %s", folder) + } + } + + return nil +} diff --git a/cmd/run-cli/config/home_nix.go b/cmd/run-cli/config/home_nix.go new file mode 100644 index 00000000..4a3c6ad6 --- /dev/null +++ b/cmd/run-cli/config/home_nix.go @@ -0,0 +1,20 @@ +//go:build !windows +// +build !windows + +package config + +import ( + "fmt" + "os" + "path/filepath" +) + +func getHomeDir() (string, error) { + home := os.Getenv("HOME") + + if home == "" { + return "", fmt.Errorf("failed to get home folder") + } + + return filepath.Join(home, ".local", "share", DefaultAppFolder), nil +} diff --git a/cmd/run-cli/config/home_windows.go b/cmd/run-cli/config/home_windows.go new file mode 100644 index 00000000..2f34224c --- /dev/null +++ b/cmd/run-cli/config/home_windows.go @@ -0,0 +1,18 @@ +//go:build windows && !linux +// +build windows,!linux + +package config + +import ( + "os" +) + +func getHomeDir() (string, error) { + appData := os.Getenv("APPDATA") + + if appData == "" { + return "", fmt.Errorf("failed to get home folder") + } + + return filepath.Join(appData, DefaultAppFolder), nil +} diff --git a/cmd/run-cli/main.go b/cmd/run-cli/main.go new file mode 100644 index 00000000..0d118156 --- /dev/null +++ b/cmd/run-cli/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2022 NAME HERE + +*/ +package main + +import "runner-manager/cmd/run-cli/cmd" + +func main() { + cmd.Execute() +} diff --git a/cmd/runner-manager/main.go b/cmd/runner-manager/main.go index b9abb589..934f349c 100644 --- a/cmd/runner-manager/main.go +++ b/cmd/runner-manager/main.go @@ -14,8 +14,11 @@ import ( "runner-manager/auth" "runner-manager/config" "runner-manager/database" + "runner-manager/database/common" "runner-manager/runner" "runner-manager/util" + + "github.com/pkg/errors" // "github.com/google/go-github/v43/github" // "golang.org/x/oauth2" // "gopkg.in/yaml.v3" @@ -28,7 +31,17 @@ var ( var Version string -// var token = "super secret token" +func maybeInitController(db common.Store) error { + if _, err := db.ControllerInfo(); err == nil { + return nil + } + + if _, err := db.InitController(); err != nil { + return errors.Wrap(err, "initializing controller") + } + + return nil +} func main() { flag.Parse() @@ -51,6 +64,15 @@ func main() { } log.SetOutput(logWriter) + db, err := database.NewDatabase(ctx, cfg.Database) + if err != nil { + log.Fatal(err) + } + + if err := maybeInitController(db); err != nil { + log.Fatal(err) + } + runner, err := runner.NewRunner(ctx, *cfg) if err != nil { log.Fatalf("failed to create controller: %+v", err) @@ -62,11 +84,6 @@ func main() { log.Fatal(err) } - db, err := database.NewDatabase(ctx, cfg.Database) - if err != nil { - log.Fatal(err) - } - authenticator := auth.NewAuthenticator(cfg.JWTAuth, db) controller, err := controllers.NewAPIController(runner, authenticator) if err != nil { diff --git a/database/sql/models.go b/database/sql/models.go index df6efa46..ccd463be 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -88,8 +88,8 @@ type Address struct { type Instance struct { Base - ProviderID string `gorm:"uniqueIndex"` - Name string `gorm:"uniqueIndex"` + ProviderID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex"` OSType config.OSType OSArch config.OSArch OSName string diff --git a/database/sql/sql.go b/database/sql/sql.go index b36e18f1..89fd1fd5 100644 --- a/database/sql/sql.go +++ b/database/sql/sql.go @@ -749,9 +749,13 @@ func (s *sqlDatabase) sqlAddressToParamsAddress(addr Address) params.Address { } func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance { + var id string + if instance.ProviderID != nil { + id = *instance.ProviderID + } ret := params.Instance{ ID: instance.ID.String(), - ProviderID: instance.ProviderID, + ProviderID: id, Name: instance.Name, OSType: instance.OSType, OSName: instance.OSName, @@ -883,7 +887,7 @@ func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, par } if param.ProviderID != "" { - instance.ProviderID = param.ProviderID + instance.ProviderID = ¶m.ProviderID } if param.OSName != "" { @@ -997,7 +1001,7 @@ func (s *sqlDatabase) updatePool(pool Pool, param params.UpdatePoolParams) (para return params.Pool{}, errors.Wrap(q.Error, "saving database entry") } - if len(param.Tags) > 0 { + if param.Tags != nil && len(param.Tags) > 0 { tags := make([]Tag, len(param.Tags)) for idx, t := range param.Tags { tags[idx] = Tag{ @@ -1099,7 +1103,7 @@ func (s *sqlDatabase) CreateUser(ctx context.Context, user params.NewUserParams) if q.Error != nil { return params.User{}, errors.Wrap(q.Error, "creating user") } - return params.User{}, nil + return s.sqlToParamsUser(newUser), nil } func (s *sqlDatabase) HasAdminUser(ctx context.Context) bool { diff --git a/go.mod b/go.mod index f912c9c4..109495fe 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,18 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 + github.com/go-resty/resty/v2 v2.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github/v43 v43.0.0 github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 + github.com/jedib0t/go-pretty/v6 v6.3.1 github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 github.com/pkg/errors v0.9.1 github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b + github.com/spf13/cobra v1.4.0 golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 gopkg.in/natefinch/lumberjack.v2 v2.0.0 @@ -30,18 +33,22 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/juju/webbrowser v1.0.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-sqlite3 v1.14.12 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/sftp v1.13.4 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect diff --git a/go.sum b/go.sum index 0a6909d2..144af30c 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -64,6 +65,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -138,6 +141,10 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jedib0t/go-pretty/v6 v6.3.1 h1:aOXiD9oqiuLH8btPQW6SfgtQN5zwhyfzZls8a6sPJ/I= +github.com/jedib0t/go-pretty/v6 v6.3.1/go.mod h1:FMkOpgGD3EZ91cW8g/96RfxoV7bdeJyzXPYgz1L1ln0= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -174,6 +181,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 h1:EmjWCASxSUz+ymsEJfiWN3yx3yTypoKJrnOSSzAWYds= github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124/go.mod h1:T4xjj62BmFg1L5JfY2wRyPZtKbBeTFgo/GLwV8DFZ8M= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= @@ -184,21 +193,29 @@ github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -282,6 +299,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -299,6 +317,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -327,6 +346,7 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -338,6 +358,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -492,6 +513,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/runner/common/pool.go b/runner/common/pool.go index f940e3e4..49df6aae 100644 --- a/runner/common/pool.go +++ b/runner/common/pool.go @@ -1,7 +1,6 @@ package common import ( - "context" "runner-manager/params" ) @@ -16,7 +15,7 @@ type PoolManager interface { WebhookSecret() string HandleWorkflowJob(job params.WorkflowJob) error RefreshState(cfg params.Repository) error - AddPool(ctx context.Context, pool params.Pool) error + // AddPool(ctx context.Context, pool params.Pool) error // PoolManager lifecycle functions. Start/stop pool. Start() error diff --git a/runner/pool/repository.go b/runner/pool/repository.go index 1064fd61..c172d13c 100644 --- a/runner/pool/repository.go +++ b/runner/pool/repository.go @@ -30,11 +30,7 @@ func NewRepositoryPoolManager(ctx context.Context, cfg params.Repository, provid if err != nil { return nil, errors.Wrap(err, "getting github client") } - pools := map[string]params.Pool{} - for _, val := range cfg.Pools { - pools[val.ID] = val - } repo := &Repository{ ctx: ctx, cfg: cfg, @@ -42,16 +38,11 @@ func NewRepositoryPoolManager(ctx context.Context, cfg params.Repository, provid id: cfg.ID, store: store, providers: providers, - pools: pools, controllerID: cfg.Internal.ControllerID, quit: make(chan struct{}), done: make(chan struct{}), } - if err := repo.loadPools(); err != nil { - return nil, errors.Wrap(err, "loading pools") - } - return repo, nil } @@ -66,7 +57,6 @@ type Repository struct { quit chan struct{} done chan struct{} id string - pools map[string]params.Pool mux sync.Mutex } @@ -84,32 +74,6 @@ func (r *Repository) RefreshState(cfg params.Repository) error { return nil } -func (r *Repository) loadPools() error { - pools, err := r.store.ListRepoPools(r.ctx, r.id) - if err != nil { - return errors.Wrap(err, "fetching pools") - } - - for _, pool := range pools { - if err := r.AddPool(r.ctx, pool); err != nil { - return errors.Wrap(err, "adding pool") - } - } - return nil -} - -func (r *Repository) AddPool(ctx context.Context, pool params.Pool) error { - r.mux.Lock() - defer r.mux.Unlock() - - if _, ok := r.pools[pool.ID]; ok { - return nil - } - - r.pools[pool.ID] = pool - return nil -} - func (r *Repository) getGithubRunners() ([]*github.Runner, error) { runners, _, err := r.ghcli.Actions.ListRunners(r.ctx, r.cfg.Owner, r.cfg.Name, nil) if err != nil { @@ -249,9 +213,12 @@ func (r *Repository) cleanupOrphanedGithubRunners(runners []*github.Runner) erro continue } - pool, ok := r.pools[poolID] - if !ok { - // not a pool we manage. + pool, err := r.store.GetRepositoryPool(r.ctx, r.id, poolID) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + return errors.Wrap(err, "fetching pool") + } + // not pool we manage. continue } @@ -340,21 +307,26 @@ func (r *Repository) cleanupOrphanedProviderRunners(runners []*github.Runner) er } func (r *Repository) ensureMinIdleRunners() { - for poolID, pool := range r.pools { + pools, err := r.store.ListRepoPools(r.ctx, r.id) + if err != nil { + log.Printf("error listing pools: %s", err) + return + } + for _, pool := range pools { if !pool.Enabled { log.Printf("pool %s is disabled, skipping", pool.ID) continue } - existingInstances, err := r.store.ListInstances(r.ctx, poolID) + existingInstances, err := r.store.ListInstances(r.ctx, pool.ID) if err != nil { - log.Printf("failed to ensure minimum idle workers for pool %s: %s", poolID, err) + log.Printf("failed to ensure minimum idle workers for pool %s: %s", pool.ID, err) return } // asJs, _ := json.MarshalIndent(existingInstances, "", " ") // log.Printf(">>> %s", string(asJs)) if uint(len(existingInstances)) >= pool.MaxRunners { - log.Printf("max workers (%d) reached for pool %s, skipping idle worker creation", pool.MaxRunners, poolID) + log.Printf("max workers (%d) reached for pool %s, skipping idle worker creation", pool.MaxRunners, pool.ID) continue } @@ -379,9 +351,9 @@ func (r *Repository) ensureMinIdleRunners() { } for i := 0; i < required; i++ { - log.Printf("addind new idle worker to pool %s", poolID) - if err := r.AddRunner(r.ctx, poolID); err != nil { - log.Printf("failed to add new instance for pool %s: %s", poolID, err) + log.Printf("addind new idle worker to pool %s", pool.ID) + if err := r.AddRunner(r.ctx, pool.ID); err != nil { + log.Printf("failed to add new instance for pool %s: %s", pool.ID, err) } } } @@ -418,9 +390,9 @@ func (r *Repository) updateArgsFromProviderInstance(providerInstance params.Inst } func (r *Repository) deleteInstanceFromProvider(instance params.Instance) error { - pool, ok := r.pools[instance.PoolID] - if !ok { - return runnerErrors.NewNotFoundError("invalid pool ID") + pool, err := r.store.GetRepositoryPool(r.ctx, r.id, instance.PoolID) + if err != nil { + return errors.Wrap(err, "fetching pool") } provider, ok := r.providers[pool.ProviderName] @@ -439,9 +411,9 @@ func (r *Repository) deleteInstanceFromProvider(instance params.Instance) error } func (r *Repository) addInstanceToProvider(instance params.Instance) error { - pool, ok := r.pools[instance.PoolID] - if !ok { - return runnerErrors.NewNotFoundError("invalid pool ID: %s", instance.PoolID) + pool, err := r.store.GetRepositoryPool(r.ctx, r.id, instance.PoolID) + if err != nil { + return errors.Wrap(err, "fetching pool") } provider, ok := r.providers[pool.ProviderName] @@ -495,9 +467,9 @@ func (r *Repository) addInstanceToProvider(instance params.Instance) error { // TODO: add function to set runner status to idle when instance calls home on callback url func (r *Repository) AddRunner(ctx context.Context, poolID string) error { - pool, ok := r.pools[poolID] - if !ok { - return runnerErrors.NewNotFoundError("invalid provider ID") + pool, err := r.store.GetRepositoryPool(r.ctx, r.id, poolID) + if err != nil { + return errors.Wrap(err, "fetching pool") } name := fmt.Sprintf("runner-manager-%s", uuid.New()) @@ -512,7 +484,7 @@ func (r *Repository) AddRunner(ctx context.Context, poolID string) error { CallbackURL: r.cfg.Internal.InstanceCallbackURL, } - _, err := r.store.CreateInstance(r.ctx, poolID, createParams) + _, err = r.store.CreateInstance(r.ctx, poolID, createParams) if err != nil { return errors.Wrap(err, "creating instance") } @@ -592,6 +564,7 @@ func (r *Repository) setInstanceRunnerStatus(job params.WorkflowJob, status prov RunnerStatus: status, } + log.Printf("setting runner status for %s to %s", runner.Name, status) if _, err := r.store.UpdateInstance(r.ctx, runner.ID, updateParams); err != nil { return errors.Wrap(err, "updating runner state") } @@ -625,9 +598,10 @@ func (r *Repository) acquireNewInstance(job params.WorkflowJob) error { if err != nil { return errors.Wrap(err, "fetching suitable pool") } + log.Printf("adding new runner with requested tags %s in pool %s", strings.Join(job.WorkflowJob.Labels, ", "), pool.ID) if !pool.Enabled { - log.Printf("selecte pool (%s) is disabled", pool.ID) + log.Printf("selected pool (%s) is disabled", pool.ID) return nil } diff --git a/runner/providers/lxd/lxd.go b/runner/providers/lxd/lxd.go index bbda83d7..2c9f8fe9 100644 --- a/runner/providers/lxd/lxd.go +++ b/runner/providers/lxd/lxd.go @@ -14,7 +14,6 @@ import ( lxd "github.com/lxc/lxd/client" "github.com/lxc/lxd/shared/api" "github.com/pkg/errors" - "gopkg.in/yaml.v3" ) var _ common.Provider = &LXD{} @@ -159,8 +158,8 @@ func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownlo continue } - fmt.Println(*tool.Architecture, *tool.OS) - fmt.Printf("image arch: %s --> osType: %s\n", image.Architecture, string(osType)) + // fmt.Println(*tool.Architecture, *tool.OS) + // fmt.Printf("image arch: %s --> osType: %s\n", image.Architecture, string(osType)) if *tool.Architecture == image.Architecture && *tool.OS == string(osType) { return *tool, nil } @@ -278,8 +277,8 @@ func (l *LXD) CreateInstance(ctx context.Context, bootstrapParams params.Bootstr return params.Instance{}, errors.Wrap(err, "fetching create args") } - asJs, err := yaml.Marshal(args) - fmt.Println(string(asJs), err) + // asJs, err := yaml.Marshal(args) + // fmt.Println(string(asJs), err) if err := l.launchInstance(args); err != nil { return params.Instance{}, errors.Wrap(err, "creating instance") } diff --git a/runner/repositories.go b/runner/repositories.go index fefb2d44..6330facc 100644 --- a/runner/repositories.go +++ b/runner/repositories.go @@ -174,7 +174,7 @@ func (r *Runner) CreateRepoPool(ctx context.Context, repoID string, param params r.mux.Lock() defer r.mux.Unlock() - repo, ok := r.repositories[repoID] + _, ok := r.repositories[repoID] if !ok { return params.Pool{}, runnerErrors.ErrNotFound } @@ -196,6 +196,13 @@ func (r *Runner) CreateRepoPool(ctx context.Context, repoID string, param params return params.Pool{}, runnerErrors.NewBadRequestError("no such provider %s", param.ProviderName) } + // github automatically adds the "self-hosted" tag as well as the OS type (linux, windows, etc) + // and architecture (arm, x64, etc) to all self hosted runners. When a workflow job comes in, we try + // to find a pool based on the labels that are set in the workflow. If we don't explicitly define these + // default tags for each pool, and the user targets these labels, we won't be able to match any pools. + // The downside is that all pools with the same OS and arch will have these default labels. Users should + // set distinct and unique labels on each pool, and explicitly target those labels, or risk assigning + // the job to the wrong worker type. ghArch, err := util.ResolveToGithubArch(string(param.OSArch)) if err != nil { return params.Pool{}, errors.Wrap(err, "invalid arch") @@ -219,23 +226,67 @@ func (r *Runner) CreateRepoPool(ctx context.Context, repoID string, param params return params.Pool{}, errors.Wrap(err, "creating pool") } - if err := repo.AddPool(ctx, pool); err != nil { - return params.Pool{}, errors.Wrap(err, "adding pool to manager") - } - return pool, nil } -func (r *Runner) DeleteRepoPool(ctx context.Context) error { +func (r *Runner) GetRepoPoolByID(ctx context.Context, repoID, poolID string) (params.Pool, error) { + if !auth.IsAdmin(ctx) { + return params.Pool{}, runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetRepositoryPool(ctx, repoID, poolID) + if err != nil { + return params.Pool{}, errors.Wrap(err, "fetching pool") + } + return pool, nil +} + +func (r *Runner) DeleteRepoPool(ctx context.Context, repoID, poolID string) error { + if !auth.IsAdmin(ctx) { + return runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetRepositoryPool(ctx, repoID, poolID) + if err != nil { + if !errors.Is(err, runnerErrors.ErrNotFound) { + return errors.Wrap(err, "fetching pool") + } + return nil + } + + instances, err := r.store.ListInstances(ctx, pool.ID) + if err != nil { + return errors.Wrap(err, "fetching instances") + } + + // TODO: implement a count function + if len(instances) > 0 { + runnerIDs := []string{} + for _, run := range instances { + runnerIDs = append(runnerIDs, run.ID) + } + return runnerErrors.NewBadRequestError("pool has runners: %s", strings.Join(runnerIDs, ", ")) + } + + if err := r.store.DeleteRepositoryPool(ctx, repoID, poolID); err != nil { + // deleted by some othe call? + if !errors.Is(err, runnerErrors.ErrNotFound) { + return errors.Wrap(err, "deleting pool") + } + } return nil } -func (r *Runner) ListRepoPools(ctx context.Context) error { - return nil -} +func (r *Runner) ListRepoPools(ctx context.Context, repoID string) ([]params.Pool, error) { + if !auth.IsAdmin(ctx) { + return []params.Pool{}, runnerErrors.ErrUnauthorized + } -func (r *Runner) UpdateRepoPool(ctx context.Context) error { - return nil + pools, err := r.store.ListRepoPools(ctx, repoID) + if err != nil { + return nil, errors.Wrap(err, "fetching pools") + } + return pools, nil } func (r *Runner) ListPoolInstances(ctx context.Context) error { @@ -269,3 +320,34 @@ func (r *Runner) findRepoPoolManager(owner, name string) (common.PoolManager, er } return nil, errors.Wrapf(runnerErrors.ErrNotFound, "repository %s/%s not configured", owner, name) } + +func (r *Runner) UpdateRepoPool(ctx context.Context, repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) { + if !auth.IsAdmin(ctx) { + return params.Pool{}, runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetRepositoryPool(ctx, repoID, poolID) + if err != nil { + return params.Pool{}, errors.Wrap(err, "fetching pool") + } + + maxRunners := pool.MaxRunners + minIdleRunners := pool.MinIdleRunners + + if param.MaxRunners != nil { + maxRunners = *param.MaxRunners + } + if param.MinIdleRunners != nil { + minIdleRunners = *param.MinIdleRunners + } + + if minIdleRunners < maxRunners { + return params.Pool{}, runnerErrors.NewBadRequestError("min_idle_runners cannot be larger than max_runners") + } + + newPool, err := r.store.UpdateRepositoryPool(ctx, repoID, poolID, param) + if err != nil { + return params.Pool{}, errors.Wrap(err, "updating pool") + } + return newPool, nil +}