diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go new file mode 100644 index 00000000..91eb2b28 --- /dev/null +++ b/apiserver/controllers/controllers.go @@ -0,0 +1,124 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "runner-manager/apiserver/params" + gErrors "runner-manager/errors" + "runner-manager/github" + "runner-manager/runner" + + "github.com/pkg/errors" +) + +func NewAPIController(r *runner.Runner) (*APIController, error) { + return &APIController{ + r: r, + }, nil +} + +type APIController struct { + r *runner.Runner +} + +func handleError(w http.ResponseWriter, err error) { + w.Header().Add("Content-Type", "application/json") + origErr := errors.Cause(err) + apiErr := params.APIErrorResponse{ + Details: origErr.Error(), + } + + switch origErr.(type) { + case *gErrors.NotFoundError: + w.WriteHeader(http.StatusNotFound) + apiErr.Error = "Not Found" + case *gErrors.UnauthorizedError: + w.WriteHeader(http.StatusUnauthorized) + apiErr.Error = "Not Authorized" + case *gErrors.BadRequestError: + w.WriteHeader(http.StatusBadRequest) + apiErr.Error = "Bad Request" + case *gErrors.DuplicateUserError, *gErrors.ConflictError: + w.WriteHeader(http.StatusConflict) + apiErr.Error = "Conflict" + default: + w.WriteHeader(http.StatusInternalServerError) + apiErr.Error = "Server error" + } + + json.NewEncoder(w).Encode(apiErr) +} + +func (a *APIController) authenticateHook(body []byte, headers http.Header) error { + // signature := headers.Get("X-Hub-Signature-256") + hookType := headers.Get("X-Github-Hook-Installation-Target-Type") + var workflowJob github.WorkflowJob + if err := json.Unmarshal(body, &workflowJob); err != nil { + return gErrors.NewBadRequestError("invalid post body: %s", err) + } + + switch hookType { + case "repository": + case "organization": + default: + return gErrors.NewBadRequestError("invalid hook type: %s", hookType) + } + return nil +} + +func (a *APIController) handleWorkflowJobEvent(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + handleError(w, gErrors.NewBadRequestError("invalid post body: %s", err)) + return + } + + signature := r.Header.Get("X-Hub-Signature-256") + hookType := r.Header.Get("X-Github-Hook-Installation-Target-Type") + fmt.Printf(">>> Signature: %s\n", signature) + fmt.Printf(">>> HookType: %s\n", hookType) + + var workflowJob github.WorkflowJob + if err := json.Unmarshal(body, &workflowJob); err != nil { + handleError(w, gErrors.ErrBadRequest) + return + } + // entity := workflowJob.Repository.Owner.Login + + asJs, err := json.MarshalIndent(workflowJob, "", " ") + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("%s\n", string(asJs)) +} + +func (a *APIController) CatchAll(w http.ResponseWriter, r *http.Request) { + headers := r.Header.Clone() + for key, val := range headers { + fmt.Printf("%s --> %v\n", key, val) + } + event := github.Event(headers.Get("X-Github-Event")) + switch event { + case github.WorkflowJobEvent: + a.handleWorkflowJobEvent(w, r) + default: + log.Printf("ignoring unknown event %s", event) + return + } +} + +// NotFoundHandler is returned when an invalid URL is acccessed +func (a *APIController) NotFoundHandler(w http.ResponseWriter, r *http.Request) { + apiErr := params.APIErrorResponse{ + Details: "Resource not found", + Error: "Not found", + } + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(apiErr) +} diff --git a/apiserver/params/params.go b/apiserver/params/params.go new file mode 100644 index 00000000..e88e26ea --- /dev/null +++ b/apiserver/params/params.go @@ -0,0 +1,7 @@ +package params + +// APIErrorResponse holds information about an error, returned by the API +type APIErrorResponse struct { + Error string `json:"error"` + Details string `json:"details"` +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go new file mode 100644 index 00000000..1b02fe92 --- /dev/null +++ b/apiserver/routers/routers.go @@ -0,0 +1,21 @@ +package routers + +import ( + "io" + "net/http" + + gorillaHandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" + + "runner-manager/apiserver/controllers" +) + +func NewAPIRouter(han *controllers.APIController, logWriter io.Writer) *mux.Router { + router := mux.NewRouter() + log := gorillaHandlers.CombinedLoggingHandler + apiRouter := router.PathPrefix("").Subrouter() + + apiRouter.PathPrefix("/").Handler(log(logWriter, http.HandlerFunc(han.CatchAll))) + + return router +} diff --git a/cmd/runner-manager/create.go b/cmd/runner-manager/create.go new file mode 100644 index 00000000..67f6e18b --- /dev/null +++ b/cmd/runner-manager/create.go @@ -0,0 +1,172 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os/signal" + + "runner-manager/config" + "runner-manager/params" + "runner-manager/runner/providers/lxd" + "runner-manager/util" + + "github.com/google/go-github/v43/github" + "golang.org/x/oauth2" + "gopkg.in/yaml.v3" +) + +var ( + conf = flag.String("config", config.DefaultConfigFilePath, "runner-manager config file") + version = flag.Bool("version", false, "prints version") +) + +var Version string + +// var token = "super secret token" + +func main() { + flag.Parse() + if *version { + fmt.Println(Version) + return + } + ctx, stop := signal.NotifyContext(context.Background(), signals...) + defer stop() + fmt.Println(ctx) + + cfg, err := config.NewConfig(*conf) + if err != nil { + log.Fatalf("Fetching config: %+v", err) + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: cfg.Github.OAuth2Token}, + ) + + tc := oauth2.NewClient(ctx, ts) + + ghClient := github.NewClient(tc) + + // // list all repositories for the authenticated user + // repos, _, err := client.Repositories.List(ctx, "", nil) + + // fmt.Println(repos, err) + + logWriter, err := util.GetLoggingWriter(cfg) + if err != nil { + log.Fatalf("fetching log writer: %+v", err) + } + log.SetOutput(logWriter) + + // controller, err := controllers.NewAPIController() + // if err != nil { + // log.Fatalf("failed to create controller: %+v", err) + // } + + // router := routers.NewAPIRouter(controller, logWriter) + + // tlsCfg, err := cfg.APIServer.APITLSConfig() + // if err != nil { + // log.Fatalf("failed to get TLS config: %q", err) + // } + + // srv := &http.Server{ + // Addr: cfg.APIServer.BindAddress(), + // TLSConfig: tlsCfg, + // // Pass our instance of gorilla/mux in. + // Handler: router, + // } + + // listener, err := net.Listen("tcp", srv.Addr) + // if err != nil { + // log.Fatalf("creating listener: %q", err) + // } + + // go func() { + // if err := srv.Serve(listener); err != nil { + // log.Fatalf("Listening: %+v", err) + // } + // }() + + // <-ctx.Done() + + // runner, err := runner.NewRunner(ctx, *cfg) + // if err != nil { + // log.Fatal(err) + // } + + // fmt.Println(runner) + controllerID := "026d374d-6a8a-4241-8ed9-a246fff6762f" + provider, err := lxd.NewProvider(ctx, &cfg.Providers[0], controllerID) + if err != nil { + log.Fatal(err) + } + + // if err := provider.RemoveAllInstances(ctx); err != nil { + // log.Fatal(err) + // } + + // fmt.Println(provider) + + // if err := provider.DeleteInstance(ctx, "runner-manager-2fbe5354-be28-4e00-95a8-11479912368d"); err != nil { + // log.Fatal(err) + // } + + // instances, err := provider.ListInstances(ctx) + + // asJs, err := json.MarshalIndent(instances, "", " ") + // fmt.Println(string(asJs), err) + + log.Print("Fetching tools") + tools, _, err := ghClient.Actions.ListOrganizationRunnerApplicationDownloads(ctx, cfg.Organizations[0].Name) + // tools, _, err := ghClient.Actions.ListRunnerApplicationDownloads(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) + if err != nil { + log.Fatal(err) + } + + tk, _, err := ghClient.Actions.CreateOrganizationRegistrationToken(ctx, "gsamfira") + + if err != nil { + log.Fatalf("fetching org token: %+v", err) + } + + fmt.Printf("Org token is: %v\n", *tk) + + toolsAsYaml, err := yaml.Marshal(tools) + if err != nil { + log.Fatal(err) + } + log.Printf("got tools:\n%s\n", string(toolsAsYaml)) + + log.Print("fetching runner token") + ghRunnerToken, _, err := ghClient.Actions.CreateRegistrationToken(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) + if err != nil { + log.Fatal(err) + } + log.Printf("got token %v", ghRunnerToken) + + bootstrapArgs := params.BootstrapInstance{ + Tools: tools, + RepoURL: cfg.Organizations[0].String(), + GithubRunnerAccessToken: *tk.Token, + RunnerType: cfg.Repositories[0].Pool.Runners[0].Name, + CallbackURL: "", + InstanceToken: "", + OSArch: config.Amd64, + Flavor: cfg.Organizations[0].Pool.Runners[0].Flavor, + Image: cfg.Organizations[0].Pool.Runners[0].Image, + Labels: cfg.Organizations[0].Pool.Runners[0].Labels, + SSHKeys: []string{ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC2oT7j/+elHY9U2ibgk2RYJgCvqIwewYKJTtHslTQFDWlHLeDam93BBOFlQJm9/wKX/qjC8d26qyzjeeeVf2EEAztp+jQfEq9OU+EtgQUi589jxtVmaWuYED8KVNbzLuP79SrBtEZD4xqgmnNotPhRshh3L6eYj4XzLWDUuOD6kzNdsJA2QOKeMOIFpBN6urKJHRHYD+oUPUX1w5QMv1W1Srlffl4m5uE+0eJYAMr02980PG4+jS4bzM170wYdWwUI0pSZsEDC8Fn7jef6QARU2CgHJYlaTem+KWSXislOUTaCpR0uhakP1ezebW20yuuc3bdRNgSlZi9B7zAPALGZpOshVqwF+KmLDi6XiFwG+NnwAFa6zaQfhOxhw/rF5Jk/wVjHIHkNNvYewycZPbKui0E3QrdVtR908N3VsPtLhMQ59BEMl3xlURSi0fiOU3UjnwmOkOoFDy/WT8qk//gFD93tUxlf4eKXDgNfME3zNz8nVi2uCPvG5NT/P/VWR8NMqW6tZcmWyswM/GgL6Y84JQ3ESZq/7WvAetdc1gVIDQJ2ejYbSHBcQpWvkocsiuMTCwiEvQ0sr+UE5jmecQvLPUyXOhuMhw43CwxnLk1ZSeYeCorxbskyqIXH71o8zhbPoPiEbwgB+i9WEoq02u7c8CmCmO8Y9aOnh8MzTKxIgQ==", + }, + } + + instance, err := provider.CreateInstance(ctx, bootstrapArgs) + if err != nil { + log.Fatal(err) + } + + fmt.Println(instance) +} diff --git a/cmd/runner-manager/main.go b/cmd/runner-manager/main.go index f26b8e15..02e96954 100644 --- a/cmd/runner-manager/main.go +++ b/cmd/runner-manager/main.go @@ -1,127 +1,160 @@ package main -import ( - "context" - "flag" - "fmt" - "log" - "os/signal" +// import ( +// "context" +// "flag" +// "fmt" +// "log" +// "net" +// "net/http" +// "os/signal" - "runner-manager/config" - "runner-manager/runner" - "runner-manager/runner/providers/lxd" - "runner-manager/util" - // "github.com/google/go-github/v43/github" - // "golang.org/x/oauth2" - // "gopkg.in/yaml.v3" -) +// "runner-manager/apiserver/controllers" +// "runner-manager/apiserver/routers" +// "runner-manager/config" +// "runner-manager/util" +// // "github.com/google/go-github/v43/github" +// // "golang.org/x/oauth2" +// // "gopkg.in/yaml.v3" +// ) -var ( - conf = flag.String("config", config.DefaultConfigFilePath, "runner-manager config file") - version = flag.Bool("version", false, "prints version") -) +// var ( +// conf = flag.String("config", config.DefaultConfigFilePath, "runner-manager config file") +// version = flag.Bool("version", false, "prints version") +// ) -var Version string +// var Version string -// var token = "super secret token" +// // var token = "super secret token" -func main() { - flag.Parse() - if *version { - fmt.Println(Version) - return - } +// func main() { +// flag.Parse() +// if *version { +// fmt.Println(Version) +// return +// } +// ctx, stop := signal.NotifyContext(context.Background(), signals...) +// defer stop() +// fmt.Println(ctx) - ctx, stop := signal.NotifyContext(context.Background(), signals...) - defer stop() - fmt.Println(ctx) +// cfg, err := config.NewConfig(*conf) +// if err != nil { +// log.Fatalf("Fetching config: %+v", err) +// } - cfg, err := config.NewConfig(*conf) - if err != nil { - log.Fatal(err) - } +// // ts := oauth2.StaticTokenSource( +// // &oauth2.Token{AccessToken: cfg.Github.OAuth2Token}, +// // ) - // ts := oauth2.StaticTokenSource( - // &oauth2.Token{AccessToken: cfg.Github.OAuth2Token}, - // ) +// // tc := oauth2.NewClient(ctx, ts) - // tc := oauth2.NewClient(ctx, ts) +// // ghClient := github.NewClient(tc) - // ghClient := github.NewClient(tc) +// // // list all repositories for the authenticated user +// // repos, _, err := client.Repositories.List(ctx, "", nil) - // // list all repositories for the authenticated user - // repos, _, err := client.Repositories.List(ctx, "", nil) +// // fmt.Println(repos, err) - // fmt.Println(repos, err) +// logWriter, err := util.GetLoggingWriter(cfg) +// if err != nil { +// log.Fatalf("fetching log writer: %+v", err) +// } +// log.SetOutput(logWriter) - logWriter, err := util.GetLoggingWriter(cfg) - if err != nil { - log.Fatal(err) - } - log.SetOutput(logWriter) +// controller, err := controllers.NewAPIController() +// if err != nil { +// log.Fatalf("failed to create controller: %+v", err) +// } - runner, err := runner.NewRunner(ctx, cfg) - if err != nil { - log.Fatal(err) - } +// router := routers.NewAPIRouter(controller, logWriter) - fmt.Println(runner) - controllerID := "026d374d-6a8a-4241-8ed9-a246fff6762f" - provider, err := lxd.NewProvider(ctx, &cfg.Providers[0], &cfg.Repositories[0].Pool, controllerID) - if err != nil { - log.Fatal(err) - } +// tlsCfg, err := cfg.APIServer.APITLSConfig() +// if err != nil { +// log.Fatalf("failed to get TLS config: %q", err) +// } - if err := provider.RemoveAllInstances(ctx); err != nil { - log.Fatal(err) - } +// srv := &http.Server{ +// Addr: cfg.APIServer.BindAddress(), +// TLSConfig: tlsCfg, +// // Pass our instance of gorilla/mux in. +// Handler: router, +// } - // fmt.Println(provider) +// listener, err := net.Listen("tcp", srv.Addr) +// if err != nil { +// log.Fatalf("creating listener: %q", err) +// } - // if err := provider.DeleteInstance(ctx, "runner-manager-2fbe5354-be28-4e00-95a8-11479912368d"); err != nil { - // log.Fatal(err) - // } +// go func() { +// if err := srv.Serve(listener); err != nil { +// log.Fatalf("Listening: %+v", err) +// } +// }() - // instances, err := provider.ListInstances(ctx) +// <-ctx.Done() - // asJs, err := json.MarshalIndent(instances, "", " ") - // fmt.Println(string(asJs), err) +// // runner, err := runner.NewRunner(ctx, *cfg) +// // if err != nil { +// // log.Fatal(err) +// // } - // log.Print("Fetching tools") - // tools, _, err := ghClient.Actions.ListRunnerApplicationDownloads(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) - // if err != nil { - // log.Fatal(err) - // } +// // fmt.Println(runner) +// // controllerID := "026d374d-6a8a-4241-8ed9-a246fff6762f" +// // provider, err := lxd.NewProvider(ctx, &cfg.Providers[0], controllerID) +// // if err != nil { +// // log.Fatal(err) +// // } - // toolsAsYaml, err := yaml.Marshal(tools) - // if err != nil { - // log.Fatal(err) - // } - // log.Printf("got tools:\n%s\n", string(toolsAsYaml)) +// // if err := provider.RemoveAllInstances(ctx); err != nil { +// // log.Fatal(err) +// // } - // log.Print("fetching runner token") - // ghRunnerToken, _, err := ghClient.Actions.CreateRegistrationToken(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) - // if err != nil { - // log.Fatal(err) - // } - // log.Printf("got token %v", ghRunnerToken) +// // fmt.Println(provider) - // bootstrapArgs := params.BootstrapInstance{ - // Tools: tools, - // RepoURL: cfg.Repositories[0].String(), - // GithubRunnerAccessToken: *ghRunnerToken.Token, - // RunnerType: cfg.Repositories[0].Pool.Runners[0].Name, - // CallbackURL: "", - // InstanceToken: "", - // SSHKeys: []string{ - // "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC2oT7j/+elHY9U2ibgk2RYJgCvqIwewYKJTtHslTQFDWlHLeDam93BBOFlQJm9/wKX/qjC8d26qyzjeeeVf2EEAztp+jQfEq9OU+EtgQUi589jxtVmaWuYED8KVNbzLuP79SrBtEZD4xqgmnNotPhRshh3L6eYj4XzLWDUuOD6kzNdsJA2QOKeMOIFpBN6urKJHRHYD+oUPUX1w5QMv1W1Srlffl4m5uE+0eJYAMr02980PG4+jS4bzM170wYdWwUI0pSZsEDC8Fn7jef6QARU2CgHJYlaTem+KWSXislOUTaCpR0uhakP1ezebW20yuuc3bdRNgSlZi9B7zAPALGZpOshVqwF+KmLDi6XiFwG+NnwAFa6zaQfhOxhw/rF5Jk/wVjHIHkNNvYewycZPbKui0E3QrdVtR908N3VsPtLhMQ59BEMl3xlURSi0fiOU3UjnwmOkOoFDy/WT8qk//gFD93tUxlf4eKXDgNfME3zNz8nVi2uCPvG5NT/P/VWR8NMqW6tZcmWyswM/GgL6Y84JQ3ESZq/7WvAetdc1gVIDQJ2ejYbSHBcQpWvkocsiuMTCwiEvQ0sr+UE5jmecQvLPUyXOhuMhw43CwxnLk1ZSeYeCorxbskyqIXH71o8zhbPoPiEbwgB+i9WEoq02u7c8CmCmO8Y9aOnh8MzTKxIgQ==", - // }, - // } +// // if err := provider.DeleteInstance(ctx, "runner-manager-2fbe5354-be28-4e00-95a8-11479912368d"); err != nil { +// // log.Fatal(err) +// // } - // instance, err := provider.CreateInstance(ctx, bootstrapArgs) - // if err != nil { - // log.Fatal(err) - // } +// // instances, err := provider.ListInstances(ctx) - // fmt.Println(instance) -} +// // asJs, err := json.MarshalIndent(instances, "", " ") +// // fmt.Println(string(asJs), err) + +// // log.Print("Fetching tools") +// // tools, _, err := ghClient.Actions.ListRunnerApplicationDownloads(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) +// // if err != nil { +// // log.Fatal(err) +// // } + +// // toolsAsYaml, err := yaml.Marshal(tools) +// // if err != nil { +// // log.Fatal(err) +// // } +// // log.Printf("got tools:\n%s\n", string(toolsAsYaml)) + +// // log.Print("fetching runner token") +// // ghRunnerToken, _, err := ghClient.Actions.CreateRegistrationToken(ctx, cfg.Repositories[0].Owner, cfg.Repositories[0].Name) +// // if err != nil { +// // log.Fatal(err) +// // } +// // log.Printf("got token %v", ghRunnerToken) + +// // bootstrapArgs := params.BootstrapInstance{ +// // Tools: tools, +// // RepoURL: cfg.Repositories[0].String(), +// // GithubRunnerAccessToken: *ghRunnerToken.Token, +// // RunnerType: cfg.Repositories[0].Pool.Runners[0].Name, +// // CallbackURL: "", +// // InstanceToken: "", +// // SSHKeys: []string{ +// // "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC2oT7j/+elHY9U2ibgk2RYJgCvqIwewYKJTtHslTQFDWlHLeDam93BBOFlQJm9/wKX/qjC8d26qyzjeeeVf2EEAztp+jQfEq9OU+EtgQUi589jxtVmaWuYED8KVNbzLuP79SrBtEZD4xqgmnNotPhRshh3L6eYj4XzLWDUuOD6kzNdsJA2QOKeMOIFpBN6urKJHRHYD+oUPUX1w5QMv1W1Srlffl4m5uE+0eJYAMr02980PG4+jS4bzM170wYdWwUI0pSZsEDC8Fn7jef6QARU2CgHJYlaTem+KWSXislOUTaCpR0uhakP1ezebW20yuuc3bdRNgSlZi9B7zAPALGZpOshVqwF+KmLDi6XiFwG+NnwAFa6zaQfhOxhw/rF5Jk/wVjHIHkNNvYewycZPbKui0E3QrdVtR908N3VsPtLhMQ59BEMl3xlURSi0fiOU3UjnwmOkOoFDy/WT8qk//gFD93tUxlf4eKXDgNfME3zNz8nVi2uCPvG5NT/P/VWR8NMqW6tZcmWyswM/GgL6Y84JQ3ESZq/7WvAetdc1gVIDQJ2ejYbSHBcQpWvkocsiuMTCwiEvQ0sr+UE5jmecQvLPUyXOhuMhw43CwxnLk1ZSeYeCorxbskyqIXH71o8zhbPoPiEbwgB+i9WEoq02u7c8CmCmO8Y9aOnh8MzTKxIgQ==", +// // }, +// // } + +// // instance, err := provider.CreateInstance(ctx, bootstrapArgs) +// // if err != nil { +// // log.Fatal(err) +// // } + +// // fmt.Println(instance) +// } diff --git a/config/config.go b/config/config.go index f6e14cff..e12656c6 100644 --- a/config/config.go +++ b/config/config.go @@ -84,14 +84,15 @@ type Config struct { // ConfigDir is the folder where the runner may save any aditional files // or configurations it may need. Things like auto-generated SSH keys that // may be used to access the runner instances. - ConfigDir string `toml:"config_dir" json:"config-dir"` - APIServer APIServer `toml:"apiserver" json:"apiserver"` - Database Database `toml:"database" json:"database"` - Repositories []Repository `toml:"repository" json:"repository"` - Providers []Provider `toml:"provider" json:"provider"` - Github Github `toml:"github"` + ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"` + APIServer APIServer `toml:"apiserver,omitempty" json:"apiserver,omitempty"` + Database Database `toml:"database,omitempty" json:"database,omitempty"` + Repositories []Repository `toml:"repository,omitempty" json:"repository,omitempty"` + Organizations []Organization `toml:"organization,omitempty" json:"organization,omitempty"` + Providers []Provider `toml:"provider,omitempty" json:"provider,omitempty"` + Github Github `toml:"github,omitempty"` // LogFile is the location of the log file. - LogFile string `toml:"log_file"` + LogFile string `toml:"log_file,omitempty"` } // Validate validates the config @@ -134,9 +135,59 @@ func (c *Config) Validate() error { } } + for _, org := range c.Organizations { + if err := org.Validate(); err != nil { + return errors.Wrap(err, "validating organization") + } + + // We also need to validate that the provider used for this + // repo, has been defined in the providers section. Multiple + // repos can use the same provider. + found := false + for _, provider := range c.Providers { + if provider.Name == org.Pool.ProviderName { + found = true + break + } + } + + if !found { + return fmt.Errorf("provider %s defined in org %s is not defined", org.Pool.ProviderName, org.Name) + } + } + return nil } +// Organization represents a Github organization for which we can manage runners. +type Organization struct { + // Name is the name of the organization. + Name string `toml:"name" json:"name"` + // WebsocketSecret is the shared secret used to create the hash of + // the webhook body. We use this to validate that the webhook message + // came in from the correct repo. + WebhookSecret string `toml:"webhook_secret" json:"webhook-secret"` + + // Pool is the pool defined for this repository. + Pool Pool `toml:"pool" json:"pool"` +} + +func (o *Organization) Validate() error { + if o.Name == "" { + return fmt.Errorf("missing org name") + } + + if err := o.Pool.Validate(); err != nil { + return errors.Wrap(err, "validating org pool") + } + + return nil +} + +func (o *Organization) String() string { + return fmt.Sprintf("https://github.com/%s", o.Name) +} + // Github hold configuration options specific to interacting with github. // Currently that is just a OAuth2 personal token. type Github struct { @@ -452,6 +503,19 @@ type APIServer struct { CORSOrigins []string `toml:"cors_origins" json:"cors-origins"` } +func (a *APIServer) APITLSConfig() (*tls.Config, error) { + if !a.UseTLS { + return nil, nil + } + + return a.TLSConfig.TLSConfig() +} + +// BindAddress returns a host:port string. +func (a *APIServer) BindAddress() string { + return fmt.Sprintf("%s:%d", a.Bind, a.Port) +} + // Validate validates the API server config func (a *APIServer) Validate() error { if a.UseTLS { diff --git a/errors/errors.go b/errors/errors.go index f22c103a..6e3c4081 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -3,5 +3,93 @@ package errors import "fmt" var ( - ErrNotFound = fmt.Errorf("Not found") + // ErrUnauthorized is returned when a user does not have + // authorization to perform a request + ErrUnauthorized = NewUnauthorizedError("Unauthorized") + // ErrNotFound is returned if an object is not found in + // the database. + ErrNotFound = NewNotFoundError("not found") + // ErrDuplicateUser is returned when creating a user, if the + // user already exists. + ErrDuplicateEntity = NewDuplicateUserError("duplicate") + // ErrBadRequest is returned is a malformed request is sent + ErrBadRequest = NewBadRequestError("invalid request") ) + +type baseError struct { + msg string +} + +func (b *baseError) Error() string { + return b.msg +} + +// NewUnauthorizedError returns a new UnauthorizedError +func NewUnauthorizedError(msg string) error { + return &UnauthorizedError{ + baseError{ + msg: msg, + }, + } +} + +// UnauthorizedError is returned when a request is unauthorized +type UnauthorizedError struct { + baseError +} + +// NewNotFoundError returns a new NotFoundError +func NewNotFoundError(msg string) error { + return &NotFoundError{ + baseError{ + msg: msg, + }, + } +} + +// NotFoundError is returned when a resource is not found +type NotFoundError struct { + baseError +} + +// NewDuplicateUserError returns a new DuplicateUserError +func NewDuplicateUserError(msg string) error { + return &DuplicateUserError{ + baseError{ + msg: msg, + }, + } +} + +// DuplicateUserError is returned when a duplicate user is requested +type DuplicateUserError struct { + baseError +} + +// NewBadRequestError returns a new BadRequestError +func NewBadRequestError(msg string, a ...interface{}) error { + return &BadRequestError{ + baseError{ + msg: fmt.Sprintf(msg, a...), + }, + } +} + +// BadRequestError is returned when a malformed request is received +type BadRequestError struct { + baseError +} + +// NewConflictError returns a new ConflictError +func NewConflictError(msg string, a ...interface{}) error { + return &ConflictError{ + baseError{ + msg: fmt.Sprintf(msg, a...), + }, + } +} + +// ConflictError is returned when a conflicting request is made +type ConflictError struct { + baseError +} diff --git a/github/github.go b/github/github.go new file mode 100644 index 00000000..94a30e1d --- /dev/null +++ b/github/github.go @@ -0,0 +1,195 @@ +package github + +import "time" + +type Event string + +const ( + // WorkflowJobEvent is the event set in the webhook payload from github + // when a workflow_job hook is sent. + WorkflowJobEvent Event = "workflow_job" +) + +// WorkflowJob holds the payload sent by github when a workload_job is sent. +type WorkflowJob struct { + Action string `json:"action"` + WorkflowJob struct { + ID int64 `json:"id"` + RunID int64 `json:"run_id"` + RunURL string `json:"run_url"` + RunAttempt int64 `json:"run_attempt"` + NodeID string `json:"node_id"` + HeadSha string `json:"head_sha"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + Status string `json:"status"` + Conclusion string `json:"conclusion"` + StartedAt time.Time `json:"started_at"` + CompletedAt time.Time `json:"completed_at"` + Name string `json:"name"` + Steps []struct { + Name string `json:"name"` + Status string `json:"status"` + Conclusion string `json:"conclusion"` + Number int64 `json:"number"` + StartedAt time.Time `json:"started_at"` + CompletedAt time.Time `json:"completed_at"` + } `json:"steps"` + CheckRunURL string `json:"check_run_url"` + Labels []string `json:"labels"` + RunnerID int64 `json:"runner_id"` + RunnerName string `json:"runner_name"` + RunnerGroupID int64 `json:"runner_group_id"` + RunnerGroupName string `json:"runner_group_name"` + } `json:"workflow_job"` + Repository struct { + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Private bool `json:"private"` + Owner struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"owner"` + HTMLURL string `json:"html_url"` + Description string `json:"description"` + Fork bool `json:"fork"` + URL string `json:"url"` + ForksURL string `json:"forks_url"` + KeysURL string `json:"keys_url"` + CollaboratorsURL string `json:"collaborators_url"` + TeamsURL string `json:"teams_url"` + HooksURL string `json:"hooks_url"` + IssueEventsURL string `json:"issue_events_url"` + EventsURL string `json:"events_url"` + AssigneesURL string `json:"assignees_url"` + BranchesURL string `json:"branches_url"` + TagsURL string `json:"tags_url"` + BlobsURL string `json:"blobs_url"` + GitTagsURL string `json:"git_tags_url"` + GitRefsURL string `json:"git_refs_url"` + TreesURL string `json:"trees_url"` + StatusesURL string `json:"statuses_url"` + LanguagesURL string `json:"languages_url"` + StargazersURL string `json:"stargazers_url"` + ContributorsURL string `json:"contributors_url"` + SubscribersURL string `json:"subscribers_url"` + SubscriptionURL string `json:"subscription_url"` + CommitsURL string `json:"commits_url"` + GitCommitsURL string `json:"git_commits_url"` + CommentsURL string `json:"comments_url"` + IssueCommentURL string `json:"issue_comment_url"` + ContentsURL string `json:"contents_url"` + CompareURL string `json:"compare_url"` + MergesURL string `json:"merges_url"` + ArchiveURL string `json:"archive_url"` + DownloadsURL string `json:"downloads_url"` + IssuesURL string `json:"issues_url"` + PullsURL string `json:"pulls_url"` + MilestonesURL string `json:"milestones_url"` + NotificationsURL string `json:"notifications_url"` + LabelsURL string `json:"labels_url"` + ReleasesURL string `json:"releases_url"` + DeploymentsURL string `json:"deployments_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt time.Time `json:"pushed_at"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + Homepage *string `json:"homepage"` + Size int64 `json:"size"` + StargazersCount int64 `json:"stargazers_count"` + WatchersCount int64 `json:"watchers_count"` + Language *string `json:"language"` + HasIssues bool `json:"has_issues"` + HasProjects bool `json:"has_projects"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + ForksCount int64 `json:"forks_count"` + MirrorURL *string `json:"mirror_url"` + Archived bool `json:"archived"` + Disabled bool `json:"disabled"` + OpenIssuesCount int64 `json:"open_issues_count"` + License struct { + Key string `json:"key"` + Name string `json:"name"` + SpdxID string `json:"spdx_id"` + URL string `json:"url"` + NodeID string `json:"node_id"` + } `json:"license"` + AllowForking bool `json:"allow_forking"` + IsTemplate bool `json:"is_template"` + // Topics []interface{} `json:"topics"` + Visibility string `json:"visibility"` + Forks int64 `json:"forks"` + OpenIssues int64 `json:"open_issues"` + Watchers int64 `json:"watchers"` + DefaultBranch string `json:"default_branch"` + } `json:"repository"` + Organization struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + URL string `json:"url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + HooksURL string `json:"hooks_url"` + IssuesURL string `json:"issues_url"` + MembersURL string `json:"members_url"` + PublicMembersURL string `json:"public_members_url"` + AvatarURL string `json:"avatar_url"` + Description string `json:"description"` + } `json:"organization"` + Enterprise struct { + ID int64 `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + // Description interface{} `json:"description"` + // WebsiteURL interface{} `json:"website_url"` + HTMLURL string `json:"html_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"enterprise"` + Sender struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"sender"` +} diff --git a/go.mod b/go.mod index d892f4cb..f9e00fc6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 github.com/google/go-github/v43 v43.0.0 + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 github.com/lxc/lxd v0.0.0-20220415052741-1170f2806124 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 @@ -15,6 +17,7 @@ require ( ) require ( + github.com/felixge/httpsnoop v1.0.1 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index d29495f1..b3fa3e30 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE= github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= @@ -52,6 +54,10 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc= diff --git a/params/params.go b/params/params.go index ce77b9eb..2a45de38 100644 --- a/params/params.go +++ b/params/params.go @@ -24,7 +24,7 @@ type Instance struct { // OSVersion is the version of the operating system. OSVersion string `json:"os-version,omitempty"` // OSArch is the operating system architecture. - OSArch string `json:"os-arch,omitempty"` + OSArch config.OSArch `json:"os-arch,omitempty"` // Addresses is a list of IP addresses the provider reports // for this instance. Addresses []string `json:"ip-addresses,omitempty"` @@ -53,4 +53,9 @@ type BootstrapInstance struct { // SSHKeys are the ssh public keys we may want to inject inside the runners, if the // provider supports it. SSHKeys []string `json:"ssh-keys"` + + OSArch config.OSArch `json:"arch"` + Flavor string `json:"flavor"` + Image string `json:"image"` + Labels []string `json:"labels"` } diff --git a/runner/pool/pool.go b/runner/pool/pool.go deleted file mode 100644 index 4eadf064..00000000 --- a/runner/pool/pool.go +++ /dev/null @@ -1 +0,0 @@ -package pool diff --git a/runner/pool/repository.go b/runner/pool/repository.go new file mode 100644 index 00000000..1846c9de --- /dev/null +++ b/runner/pool/repository.go @@ -0,0 +1,46 @@ +package pool + +import ( + "context" + "runner-manager/config" + "runner-manager/params" + "runner-manager/runner/common" + + "github.com/google/go-github/v43/github" +) + +func NewRepositoryRunnerPool(ctx context.Context, cfg config.Repository, ghcli *github.Client, provider common.Provider) (common.PoolManager, error) { + return &Repository{ + ctx: ctx, + cfg: cfg, + ghcli: ghcli, + provider: provider, + }, nil +} + +type Repository struct { + ctx context.Context + cfg config.Repository + ghcli *github.Client + provider common.Provider +} + +func (r *Repository) getGithubRunners() ([]github.Runner, error) { + return nil, nil +} + +func (r *Repository) getProviderInstances() ([]params.Instance, error) { + return nil, nil +} + +func (r *Repository) Start() error { + return nil +} + +func (r *Repository) Stop() error { + return nil +} + +func (r *Repository) loop() { + +} diff --git a/runner/providers/lxd/lxd.go b/runner/providers/lxd/lxd.go index 279c6a65..42bec17b 100644 --- a/runner/providers/lxd/lxd.go +++ b/runner/providers/lxd/lxd.go @@ -3,9 +3,7 @@ package lxd import ( "context" "fmt" - "strings" - "runner-manager/cloudconfig" "runner-manager/config" runnerErrors "runner-manager/errors" "runner-manager/params" @@ -47,6 +45,12 @@ var ( config.Arm64: "aarch64", config.Arm: "armv7l", } + + lxdToConfigArch map[string]config.OSArch = map[string]config.OSArch{ + "x86_64": config.Amd64, + "aarch64": config.Arm64, + "armv7l": config.Arm, + } ) const ( @@ -54,22 +58,15 @@ const ( DefaultProjectName = "runner-manager-project" ) -func NewProvider(ctx context.Context, cfg *config.Provider, pool *config.Pool, controllerID string) (common.Provider, error) { +func NewProvider(ctx context.Context, cfg *config.Provider, controllerID string) (common.Provider, error) { if err := cfg.Validate(); err != nil { return nil, errors.Wrap(err, "validating provider config") } - if err := pool.Validate(); err != nil { - return nil, errors.Wrap(err, "validating pool") - } if cfg.ProviderType != config.LXDProvider { return nil, fmt.Errorf("invalid provider type %s, expected %s", cfg.ProviderType, config.LXDProvider) } - if cfg.Name != pool.ProviderName { - return nil, fmt.Errorf("provider %s is not responsible for pool", cfg.Name) - } - cli, err := getClientFromConfig(ctx, &cfg.LXD) if err != nil { return nil, errors.Wrap(err, "creating LXD client") @@ -84,7 +81,6 @@ func NewProvider(ctx context.Context, cfg *config.Provider, pool *config.Pool, c provider := &LXD{ ctx: ctx, cfg: cfg, - pool: pool, cli: cli, controllerID: controllerID, imageManager: &image{ @@ -99,9 +95,6 @@ func NewProvider(ctx context.Context, cfg *config.Provider, pool *config.Pool, c type LXD struct { // cfg is the provider config for this provider. cfg *config.Provider - // pool holds the config for the pool this provider is - // responsible for. - pool *config.Pool // ctx is the context. ctx context.Context // cli is the LXD client. @@ -112,7 +105,7 @@ type LXD struct { controllerID string } -func (l *LXD) getProfiles(runner config.Runner) ([]string, error) { +func (l *LXD) getProfiles(flavor string) ([]string, error) { ret := []string{} if l.cfg.LXD.IncludeDefaultProfile { ret = append(ret, "default") @@ -128,45 +121,14 @@ func (l *LXD) getProfiles(runner config.Runner) ([]string, error) { set[profile] = struct{}{} } - if _, ok := set[runner.Flavor]; !ok { - return nil, errors.Wrapf(runnerErrors.ErrNotFound, "looking for profile %s", runner.Flavor) + if _, ok := set[flavor]; !ok { + return nil, errors.Wrapf(runnerErrors.ErrNotFound, "looking for profile %s", flavor) } - ret = append(ret, runner.Flavor) + ret = append(ret, flavor) return ret, nil } -func (l *LXD) getCloudConfig(runner config.Runner, bootstrapParams params.BootstrapInstance, tools github.RunnerApplicationDownload, runnerName string) (string, error) { - cloudCfg := cloudconfig.NewDefaultCloudInitConfig() - - installRunnerParams := cloudconfig.InstallRunnerParams{ - FileName: *tools.Filename, - DownloadURL: *tools.DownloadURL, - GithubToken: bootstrapParams.GithubRunnerAccessToken, - RunnerUsername: config.DefaultUser, - RunnerGroup: config.DefaultUser, - RepoURL: bootstrapParams.RepoURL, - RunnerName: runnerName, - RunnerLabels: strings.Join(runner.Labels, ","), - } - - installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams) - if err != nil { - return "", errors.Wrap(err, "generating script") - } - - cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...) - cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755") - cloudCfg.AddRunCmd("/install_runner.sh") - cloudCfg.AddRunCmd("rm -f /install_runner.sh") - - asStr, err := cloudCfg.Serialize() - if err != nil { - return "", errors.Wrap(err, "creating cloud config") - } - return asStr, nil -} - func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownload) (github.RunnerApplicationDownload, error) { if image == nil { return github.RunnerApplicationDownload{}, fmt.Errorf("nil image received") @@ -211,17 +173,6 @@ func (l *LXD) getTools(image *api.Image, tools []*github.RunnerApplicationDownlo return github.RunnerApplicationDownload{}, fmt.Errorf("failed to find tools for OS %s and arch %s", osType, image.Architecture) } -func (l *LXD) resolveArchitecture(runner config.Runner) (string, error) { - if string(runner.OSArch) == "" { - return configToLXDArchMap[config.Amd64], nil - } - arch, ok := configToLXDArchMap[runner.OSArch] - if !ok { - return "", fmt.Errorf("architecture %s is not supported", runner.OSArch) - } - return arch, nil -} - // sadly, the security.secureboot flag is a string encoded boolean. func (l *LXD) secureBootEnabled() string { if l.cfg.LXD.SecureBoot { @@ -232,23 +183,18 @@ func (l *LXD) secureBootEnabled() string { func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (api.InstancesPost, error) { name := fmt.Sprintf("runner-manager-%s", uuid.New()) - runner, err := util.FindRunnerType(bootstrapParams.RunnerType, l.pool.Runners) - if err != nil { - return api.InstancesPost{}, errors.Wrap(err, "fetching runner") - } - - profiles, err := l.getProfiles(runner) + profiles, err := l.getProfiles(bootstrapParams.Flavor) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "fetching profiles") } - arch, err := l.resolveArchitecture(runner) + arch, err := resolveArchitecture(bootstrapParams.OSArch) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "fetching archictecture") } - image, err := l.imageManager.EnsureImage(runner.Image, config.LXDImageVirtualMachine, arch) + image, err := l.imageManager.EnsureImage(bootstrapParams.Image, config.LXDImageVirtualMachine, arch) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "getting image details") } @@ -258,7 +204,7 @@ func (l *LXD) getCreateInstanceArgs(bootstrapParams params.BootstrapInstance) (a return api.InstancesPost{}, errors.Wrap(err, "getting tools") } - cloudCfg, err := l.getCloudConfig(runner, bootstrapParams, tools, name) + cloudCfg, err := util.GetCloudConfig(bootstrapParams, tools, name) if err != nil { return api.InstancesPost{}, errors.Wrap(err, "generating cloud-config") } diff --git a/runner/providers/lxd/util.go b/runner/providers/lxd/util.go index 537d44a5..36042c11 100644 --- a/runner/providers/lxd/util.go +++ b/runner/providers/lxd/util.go @@ -2,6 +2,7 @@ package lxd import ( "context" + "fmt" "io/ioutil" "log" "runner-manager/config" @@ -41,8 +42,14 @@ func lxdInstanceToAPIInstance(instance *api.InstanceFull) params.Instance { } } } + + instanceArch, ok := lxdToConfigArch[instance.Architecture] + if !ok { + log.Printf("failed to find OS architecture") + } + return params.Instance{ - OSArch: instance.Architecture, + OSArch: instanceArch, ProviderID: instance.Name, Name: instance.Name, OSType: osType, @@ -103,3 +110,14 @@ func projectName(cfg config.LXD) string { } return DefaultProjectName } + +func resolveArchitecture(osArch config.OSArch) (string, error) { + if string(osArch) == "" { + return configToLXDArchMap[config.Amd64], nil + } + arch, ok := configToLXDArchMap[osArch] + if !ok { + return "", fmt.Errorf("architecture %s is not supported", osArch) + } + return arch, nil +} diff --git a/runner/providers/providers.go b/runner/providers/providers.go new file mode 100644 index 00000000..cc0e5e0a --- /dev/null +++ b/runner/providers/providers.go @@ -0,0 +1,27 @@ +package providers + +import ( + "context" + "runner-manager/config" + "runner-manager/runner/common" + "runner-manager/runner/providers/lxd" + + "github.com/pkg/errors" +) + +// LoadProvidersFromConfig loads all providers from the config and populates +// a map with them. +func LoadProvidersFromConfig(ctx context.Context, cfg config.Config, controllerID string) (map[string]common.Provider, error) { + providers := map[string]common.Provider{} + for _, providerCfg := range cfg.Providers { + switch providerCfg.ProviderType { + case config.LXDProvider: + provider, err := lxd.NewProvider(ctx, &providerCfg, controllerID) + if err != nil { + return nil, errors.Wrap(err, "creating provider") + } + providers[providerCfg.Name] = provider + } + } + return providers, nil +} diff --git a/runner/runner.go b/runner/runner.go index 3ef49333..036293ed 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -2,11 +2,14 @@ package runner import ( "context" + "fmt" "io/ioutil" "os" "path/filepath" "runner-manager/config" + gErrors "runner-manager/errors" "runner-manager/runner/common" + "runner-manager/runner/providers" "runner-manager/util" "sync" @@ -15,10 +18,22 @@ import ( "golang.org/x/crypto/ssh" ) -func NewRunner(ctx context.Context, cfg *config.Config) (*Runner, error) { +func NewRunner(ctx context.Context, cfg config.Config) (*Runner, error) { + ghc, err := util.GithubClientFromConfig(ctx, cfg.Github) + if err != nil { + return nil, errors.Wrap(err, "getting github client") + } + + providers, err := providers.LoadProvidersFromConfig(ctx, cfg, "") + if err != nil { + return nil, errors.Wrap(err, "loading providers") + } + runner := &Runner{ - ctx: ctx, - config: cfg, + ctx: ctx, + config: cfg, + ghc: ghc, + providers: providers, } if err := runner.ensureSSHKeys(); err != nil { @@ -34,8 +49,35 @@ type Runner struct { ctx context.Context ghc *github.Client - config *config.Config - pools []common.PoolManager + controllerID string + + config config.Config + repositories map[string]common.PoolManager + organizations map[string]common.PoolManager + providers map[string]common.Provider +} + +func (r *Runner) getRepoSecret(repoName string) (string, error) { + return "", nil +} + +func (r *Runner) getOrgSecret(orgName string) (string, error) { + return "", nil +} + +func (r *Runner) ValidateHookBody(hookTargetType, signature, entity string, body []byte) error { + var secret string + var err error + switch hookTargetType { + case "repository": + secret, err = r.getRepoSecret(entity) + case "organization": + secret, err = r.getOrgSecret(entity) + default: + return gErrors.NewBadRequestError("invalid hook type: %s", hookTargetType) + } + fmt.Println(secret, err) + return nil } func (r *Runner) sshDir() string { diff --git a/util/util.go b/util/util.go index df7643b8..30ffb772 100644 --- a/util/util.go +++ b/util/util.go @@ -2,6 +2,7 @@ package util import ( "bytes" + "context" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -14,12 +15,16 @@ import ( "path" "strings" + "github.com/google/go-github/v43/github" "github.com/pkg/errors" "golang.org/x/crypto/ssh" + "golang.org/x/oauth2" lumberjack "gopkg.in/natefinch/lumberjack.v2" + "runner-manager/cloudconfig" "runner-manager/config" runnerErrors "runner-manager/errors" + "runner-manager/params" ) var ( @@ -110,3 +115,46 @@ func OSToOSType(os string) (config.OSType, error) { } return osType, nil } + +func GithubClientFromConfig(ctx context.Context, cfg config.Github) (*github.Client, error) { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: cfg.OAuth2Token}, + ) + + tc := oauth2.NewClient(ctx, ts) + + ghClient := github.NewClient(tc) + + return ghClient, nil +} + +func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.RunnerApplicationDownload, runnerName string) (string, error) { + cloudCfg := cloudconfig.NewDefaultCloudInitConfig() + + installRunnerParams := cloudconfig.InstallRunnerParams{ + FileName: *tools.Filename, + DownloadURL: *tools.DownloadURL, + GithubToken: bootstrapParams.GithubRunnerAccessToken, + RunnerUsername: config.DefaultUser, + RunnerGroup: config.DefaultUser, + RepoURL: bootstrapParams.RepoURL, + RunnerName: runnerName, + RunnerLabels: strings.Join(bootstrapParams.Labels, ","), + } + + installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams) + if err != nil { + return "", errors.Wrap(err, "generating script") + } + + cloudCfg.AddSSHKey(bootstrapParams.SSHKeys...) + cloudCfg.AddFile(installScript, "/install_runner.sh", "root:root", "755") + cloudCfg.AddRunCmd("/install_runner.sh") + cloudCfg.AddRunCmd("rm -f /install_runner.sh") + + asStr, err := cloudCfg.Serialize() + if err != nil { + return "", errors.Wrap(err, "creating cloud config") + } + return asStr, nil +}