Merge pull request #49 from gabriel-samfira/add-token-endpoint

Add a metadata resource for instances
This commit is contained in:
Gabriel 2022-12-30 01:26:52 +02:00 committed by GitHub
commit 59ddb4157a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1429 additions and 504 deletions

View file

@ -8,11 +8,13 @@ Garm enables you to create and automatically maintain pools of [self-hosted GitH
The goal of ```garm``` is to be simple to set up, simple to configure and simple to use. It is a single binary that can run on any GNU/Linux machine without any other requirements other than the providers it creates the runners in. It is intended to be easy to deploy in any environment and can create runners in any system you can write a provider for. There is no complicated setup process and no extremely complex concepts to understant. Once set up, it's meant to stay out of your way.
Garm supports creating pools on either GitHub itself or on your own deployment of [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@3.5/admin/overview/about-github-enterprise-server). For instructions on how to use ```garm``` with GHE, see the [credentials](/doc/github_credentials.md) section of the documentation.
## Installing
## Build from source
You need to have Go install, then run:
You need to have Go installed, then run:
```bash
git clone https://github.com/cloudbase/garm

View file

@ -202,3 +202,17 @@ func (a *APIController) InstanceStatusMessageHandler(w http.ResponseWriter, r *h
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := a.r.GetInstanceGithubRegistrationToken(ctx)
if err != nil {
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(token))
}

View file

@ -18,37 +18,43 @@ import (
"io"
"net/http"
gorillaHandlers "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"garm/apiserver/controllers"
"garm/auth"
"garm/util"
)
func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddleware, initMiddleware, instanceMiddleware auth.Middleware) *mux.Router {
router := mux.NewRouter()
log := gorillaHandlers.CombinedLoggingHandler
logMiddleware := util.NewLoggingMiddleware(logWriter)
router.Use(logMiddleware)
// Handles github webhooks
webhookRouter := router.PathPrefix("/webhooks").Subrouter()
webhookRouter.PathPrefix("/").Handler(log(logWriter, http.HandlerFunc(han.CatchAll)))
webhookRouter.PathPrefix("").Handler(log(logWriter, http.HandlerFunc(han.CatchAll)))
webhookRouter.PathPrefix("/").Handler(http.HandlerFunc(han.CatchAll))
webhookRouter.PathPrefix("").Handler(http.HandlerFunc(han.CatchAll))
// Handles API calls
apiSubRouter := router.PathPrefix("/api/v1").Subrouter()
// FirstRunHandler
firstRunRouter := apiSubRouter.PathPrefix("/first-run").Subrouter()
firstRunRouter.Handle("/", log(logWriter, http.HandlerFunc(han.FirstRunHandler))).Methods("POST", "OPTIONS")
firstRunRouter.Handle("/", http.HandlerFunc(han.FirstRunHandler)).Methods("POST", "OPTIONS")
// Instance callback
// Instance URLs
callbackRouter := apiSubRouter.PathPrefix("/callbacks").Subrouter()
callbackRouter.Handle("/status/", log(logWriter, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS")
callbackRouter.Handle("/status", log(logWriter, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS")
callbackRouter.Handle("/status/", http.HandlerFunc(han.InstanceStatusMessageHandler)).Methods("POST", "OPTIONS")
callbackRouter.Handle("/status", http.HandlerFunc(han.InstanceStatusMessageHandler)).Methods("POST", "OPTIONS")
callbackRouter.Use(instanceMiddleware.Middleware)
metadataRouter := apiSubRouter.PathPrefix("/metadata").Subrouter()
metadataRouter.Handle("/runner-registration-token/", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/runner-registration-token", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
metadataRouter.Use(instanceMiddleware.Middleware)
// Login
authRouter := apiSubRouter.PathPrefix("/auth").Subrouter()
authRouter.Handle("/{login:login\\/?}", log(logWriter, http.HandlerFunc(han.LoginHandler))).Methods("POST", "OPTIONS")
authRouter.Handle("/{login:login\\/?}", http.HandlerFunc(han.LoginHandler)).Methods("POST", "OPTIONS")
authRouter.Use(initMiddleware.Middleware)
apiRouter := apiSubRouter.PathPrefix("").Subrouter()
@ -59,158 +65,158 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
// Pools //
///////////
// List all pools
apiRouter.Handle("/pools/", log(logWriter, http.HandlerFunc(han.ListAllPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools", log(logWriter, http.HandlerFunc(han.ListAllPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/", http.HandlerFunc(han.ListAllPoolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools", http.HandlerFunc(han.ListAllPoolsHandler)).Methods("GET", "OPTIONS")
// Get one pool
apiRouter.Handle("/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.GetPoolByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", log(logWriter, http.HandlerFunc(han.GetPoolByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/", http.HandlerFunc(han.GetPoolByIDHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", http.HandlerFunc(han.GetPoolByIDHandler)).Methods("GET", "OPTIONS")
// Delete one pool
apiRouter.Handle("/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.DeletePoolByIDHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", log(logWriter, http.HandlerFunc(han.DeletePoolByIDHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/", http.HandlerFunc(han.DeletePoolByIDHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", http.HandlerFunc(han.DeletePoolByIDHandler)).Methods("DELETE", "OPTIONS")
// Update one pool
apiRouter.Handle("/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.UpdatePoolByIDHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", log(logWriter, http.HandlerFunc(han.UpdatePoolByIDHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/", http.HandlerFunc(han.UpdatePoolByIDHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/pools/{poolID}", http.HandlerFunc(han.UpdatePoolByIDHandler)).Methods("PUT", "OPTIONS")
// List pool instances
apiRouter.Handle("/pools/{poolID}/instances/", log(logWriter, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/instances", log(logWriter, http.HandlerFunc(han.ListPoolInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/instances/", http.HandlerFunc(han.ListPoolInstancesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/pools/{poolID}/instances", http.HandlerFunc(han.ListPoolInstancesHandler)).Methods("GET", "OPTIONS")
/////////////
// Runners //
/////////////
// Get instance
apiRouter.Handle("/instances/{instanceName}/", log(logWriter, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}", log(logWriter, http.HandlerFunc(han.GetInstanceHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}/", http.HandlerFunc(han.GetInstanceHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}", http.HandlerFunc(han.GetInstanceHandler)).Methods("GET", "OPTIONS")
// Delete runner
apiRouter.Handle("/instances/{instanceName}/", log(logWriter, http.HandlerFunc(han.DeleteInstanceHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}", log(logWriter, http.HandlerFunc(han.DeleteInstanceHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}/", http.HandlerFunc(han.DeleteInstanceHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/instances/{instanceName}", http.HandlerFunc(han.DeleteInstanceHandler)).Methods("DELETE", "OPTIONS")
// List runners
apiRouter.Handle("/instances/", log(logWriter, http.HandlerFunc(han.ListAllInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances", log(logWriter, http.HandlerFunc(han.ListAllInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances/", http.HandlerFunc(han.ListAllInstancesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/instances", http.HandlerFunc(han.ListAllInstancesHandler)).Methods("GET", "OPTIONS")
/////////////////////
// Repos and pools //
/////////////////////
// Get pool
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.GetRepoPoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.GetRepoPoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", http.HandlerFunc(han.GetRepoPoolHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", http.HandlerFunc(han.GetRepoPoolHandler)).Methods("GET", "OPTIONS")
// Delete pool
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.DeleteRepoPoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.DeleteRepoPoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", http.HandlerFunc(han.DeleteRepoPoolHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", http.HandlerFunc(han.DeleteRepoPoolHandler)).Methods("DELETE", "OPTIONS")
// Update pool
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.UpdateRepoPoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.UpdateRepoPoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}/", http.HandlerFunc(han.UpdateRepoPoolHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/{poolID}", http.HandlerFunc(han.UpdateRepoPoolHandler)).Methods("PUT", "OPTIONS")
// List pools
apiRouter.Handle("/repositories/{repoID}/pools/", log(logWriter, http.HandlerFunc(han.ListRepoPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools", log(logWriter, http.HandlerFunc(han.ListRepoPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/", http.HandlerFunc(han.ListRepoPoolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools", http.HandlerFunc(han.ListRepoPoolsHandler)).Methods("GET", "OPTIONS")
// Create pool
apiRouter.Handle("/repositories/{repoID}/pools/", log(logWriter, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools", log(logWriter, http.HandlerFunc(han.CreateRepoPoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools/", http.HandlerFunc(han.CreateRepoPoolHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/pools", http.HandlerFunc(han.CreateRepoPoolHandler)).Methods("POST", "OPTIONS")
// Repo instances list
apiRouter.Handle("/repositories/{repoID}/instances/", log(logWriter, http.HandlerFunc(han.ListRepoInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/instances", log(logWriter, http.HandlerFunc(han.ListRepoInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/instances/", http.HandlerFunc(han.ListRepoInstancesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/instances", http.HandlerFunc(han.ListRepoInstancesHandler)).Methods("GET", "OPTIONS")
// Get repo
apiRouter.Handle("/repositories/{repoID}/", log(logWriter, http.HandlerFunc(han.GetRepoByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", log(logWriter, http.HandlerFunc(han.GetRepoByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/", http.HandlerFunc(han.GetRepoByIDHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", http.HandlerFunc(han.GetRepoByIDHandler)).Methods("GET", "OPTIONS")
// Update repo
apiRouter.Handle("/repositories/{repoID}/", log(logWriter, http.HandlerFunc(han.UpdateRepoHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", log(logWriter, http.HandlerFunc(han.UpdateRepoHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/", http.HandlerFunc(han.UpdateRepoHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", http.HandlerFunc(han.UpdateRepoHandler)).Methods("PUT", "OPTIONS")
// Delete repo
apiRouter.Handle("/repositories/{repoID}/", log(logWriter, http.HandlerFunc(han.DeleteRepoHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", log(logWriter, http.HandlerFunc(han.DeleteRepoHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/", http.HandlerFunc(han.DeleteRepoHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}", http.HandlerFunc(han.DeleteRepoHandler)).Methods("DELETE", "OPTIONS")
// List repos
apiRouter.Handle("/repositories/", log(logWriter, http.HandlerFunc(han.ListReposHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories", log(logWriter, http.HandlerFunc(han.ListReposHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories/", http.HandlerFunc(han.ListReposHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/repositories", http.HandlerFunc(han.ListReposHandler)).Methods("GET", "OPTIONS")
// Create repo
apiRouter.Handle("/repositories/", log(logWriter, http.HandlerFunc(han.CreateRepoHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories", log(logWriter, http.HandlerFunc(han.CreateRepoHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/", http.HandlerFunc(han.CreateRepoHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories", http.HandlerFunc(han.CreateRepoHandler)).Methods("POST", "OPTIONS")
/////////////////////////////
// Organizations and pools //
/////////////////////////////
// Get pool
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.GetOrgPoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.GetOrgPoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", http.HandlerFunc(han.GetOrgPoolHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", http.HandlerFunc(han.GetOrgPoolHandler)).Methods("GET", "OPTIONS")
// Delete pool
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.DeleteOrgPoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.DeleteOrgPoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", http.HandlerFunc(han.DeleteOrgPoolHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", http.HandlerFunc(han.DeleteOrgPoolHandler)).Methods("DELETE", "OPTIONS")
// Update pool
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.UpdateOrgPoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.UpdateOrgPoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}/", http.HandlerFunc(han.UpdateOrgPoolHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/{poolID}", http.HandlerFunc(han.UpdateOrgPoolHandler)).Methods("PUT", "OPTIONS")
// List pools
apiRouter.Handle("/organizations/{orgID}/pools/", log(logWriter, http.HandlerFunc(han.ListOrgPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools", log(logWriter, http.HandlerFunc(han.ListOrgPoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/", http.HandlerFunc(han.ListOrgPoolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools", http.HandlerFunc(han.ListOrgPoolsHandler)).Methods("GET", "OPTIONS")
// Create pool
apiRouter.Handle("/organizations/{orgID}/pools/", log(logWriter, http.HandlerFunc(han.CreateOrgPoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools", log(logWriter, http.HandlerFunc(han.CreateOrgPoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools/", http.HandlerFunc(han.CreateOrgPoolHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/pools", http.HandlerFunc(han.CreateOrgPoolHandler)).Methods("POST", "OPTIONS")
// Repo instances list
apiRouter.Handle("/organizations/{orgID}/instances/", log(logWriter, http.HandlerFunc(han.ListOrgInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/instances", log(logWriter, http.HandlerFunc(han.ListOrgInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/instances/", http.HandlerFunc(han.ListOrgInstancesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/instances", http.HandlerFunc(han.ListOrgInstancesHandler)).Methods("GET", "OPTIONS")
// Get org
apiRouter.Handle("/organizations/{orgID}/", log(logWriter, http.HandlerFunc(han.GetOrgByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", log(logWriter, http.HandlerFunc(han.GetOrgByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/", http.HandlerFunc(han.GetOrgByIDHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", http.HandlerFunc(han.GetOrgByIDHandler)).Methods("GET", "OPTIONS")
// Update org
apiRouter.Handle("/organizations/{orgID}/", log(logWriter, http.HandlerFunc(han.UpdateOrgHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", log(logWriter, http.HandlerFunc(han.UpdateOrgHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/", http.HandlerFunc(han.UpdateOrgHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", http.HandlerFunc(han.UpdateOrgHandler)).Methods("PUT", "OPTIONS")
// Delete org
apiRouter.Handle("/organizations/{orgID}/", log(logWriter, http.HandlerFunc(han.DeleteOrgHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", log(logWriter, http.HandlerFunc(han.DeleteOrgHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/", http.HandlerFunc(han.DeleteOrgHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}", http.HandlerFunc(han.DeleteOrgHandler)).Methods("DELETE", "OPTIONS")
// List orgs
apiRouter.Handle("/organizations/", log(logWriter, http.HandlerFunc(han.ListOrgsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations", log(logWriter, http.HandlerFunc(han.ListOrgsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations/", http.HandlerFunc(han.ListOrgsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/organizations", http.HandlerFunc(han.ListOrgsHandler)).Methods("GET", "OPTIONS")
// Create org
apiRouter.Handle("/organizations/", log(logWriter, http.HandlerFunc(han.CreateOrgHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations", log(logWriter, http.HandlerFunc(han.CreateOrgHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations/", http.HandlerFunc(han.CreateOrgHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations", http.HandlerFunc(han.CreateOrgHandler)).Methods("POST", "OPTIONS")
/////////////////////////////
// Enterprises and pools //
/////////////////////////////
// Get pool
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.GetEnterprisePoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.GetEnterprisePoolHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", http.HandlerFunc(han.GetEnterprisePoolHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", http.HandlerFunc(han.GetEnterprisePoolHandler)).Methods("GET", "OPTIONS")
// Delete pool
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.DeleteEnterprisePoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.DeleteEnterprisePoolHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", http.HandlerFunc(han.DeleteEnterprisePoolHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", http.HandlerFunc(han.DeleteEnterprisePoolHandler)).Methods("DELETE", "OPTIONS")
// Update pool
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", log(logWriter, http.HandlerFunc(han.UpdateEnterprisePoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", log(logWriter, http.HandlerFunc(han.UpdateEnterprisePoolHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}/", http.HandlerFunc(han.UpdateEnterprisePoolHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/{poolID}", http.HandlerFunc(han.UpdateEnterprisePoolHandler)).Methods("PUT", "OPTIONS")
// List pools
apiRouter.Handle("/enterprises/{enterpriseID}/pools/", log(logWriter, http.HandlerFunc(han.ListEnterprisePoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools", log(logWriter, http.HandlerFunc(han.ListEnterprisePoolsHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/", http.HandlerFunc(han.ListEnterprisePoolsHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools", http.HandlerFunc(han.ListEnterprisePoolsHandler)).Methods("GET", "OPTIONS")
// Create pool
apiRouter.Handle("/enterprises/{enterpriseID}/pools/", log(logWriter, http.HandlerFunc(han.CreateEnterprisePoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools", log(logWriter, http.HandlerFunc(han.CreateEnterprisePoolHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools/", http.HandlerFunc(han.CreateEnterprisePoolHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/pools", http.HandlerFunc(han.CreateEnterprisePoolHandler)).Methods("POST", "OPTIONS")
// Repo instances list
apiRouter.Handle("/enterprises/{enterpriseID}/instances/", log(logWriter, http.HandlerFunc(han.ListEnterpriseInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/instances", log(logWriter, http.HandlerFunc(han.ListEnterpriseInstancesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/instances/", http.HandlerFunc(han.ListEnterpriseInstancesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/instances", http.HandlerFunc(han.ListEnterpriseInstancesHandler)).Methods("GET", "OPTIONS")
// Get org
apiRouter.Handle("/enterprises/{enterpriseID}/", log(logWriter, http.HandlerFunc(han.GetEnterpriseByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", log(logWriter, http.HandlerFunc(han.GetEnterpriseByIDHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/", http.HandlerFunc(han.GetEnterpriseByIDHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", http.HandlerFunc(han.GetEnterpriseByIDHandler)).Methods("GET", "OPTIONS")
// Update org
apiRouter.Handle("/enterprises/{enterpriseID}/", log(logWriter, http.HandlerFunc(han.UpdateEnterpriseHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", log(logWriter, http.HandlerFunc(han.UpdateEnterpriseHandler))).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/", http.HandlerFunc(han.UpdateEnterpriseHandler)).Methods("PUT", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", http.HandlerFunc(han.UpdateEnterpriseHandler)).Methods("PUT", "OPTIONS")
// Delete org
apiRouter.Handle("/enterprises/{enterpriseID}/", log(logWriter, http.HandlerFunc(han.DeleteEnterpriseHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", log(logWriter, http.HandlerFunc(han.DeleteEnterpriseHandler))).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}/", http.HandlerFunc(han.DeleteEnterpriseHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/enterprises/{enterpriseID}", http.HandlerFunc(han.DeleteEnterpriseHandler)).Methods("DELETE", "OPTIONS")
// List orgs
apiRouter.Handle("/enterprises/", log(logWriter, http.HandlerFunc(han.ListEnterprisesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises", log(logWriter, http.HandlerFunc(han.ListEnterprisesHandler))).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises/", http.HandlerFunc(han.ListEnterprisesHandler)).Methods("GET", "OPTIONS")
apiRouter.Handle("/enterprises", http.HandlerFunc(han.ListEnterprisesHandler)).Methods("GET", "OPTIONS")
// Create org
apiRouter.Handle("/enterprises/", log(logWriter, http.HandlerFunc(han.CreateEnterpriseHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises", log(logWriter, http.HandlerFunc(han.CreateEnterpriseHandler))).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises/", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/enterprises", http.HandlerFunc(han.CreateEnterpriseHandler)).Methods("POST", "OPTIONS")
// Credentials and providers
apiRouter.Handle("/credentials/", log(logWriter, http.HandlerFunc(han.ListCredentials))).Methods("GET", "OPTIONS")
apiRouter.Handle("/credentials", log(logWriter, http.HandlerFunc(han.ListCredentials))).Methods("GET", "OPTIONS")
apiRouter.Handle("/providers/", log(logWriter, http.HandlerFunc(han.ListProviders))).Methods("GET", "OPTIONS")
apiRouter.Handle("/providers", log(logWriter, http.HandlerFunc(han.ListProviders))).Methods("GET", "OPTIONS")
apiRouter.Handle("/credentials/", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS")
apiRouter.Handle("/credentials", http.HandlerFunc(han.ListCredentials)).Methods("GET", "OPTIONS")
apiRouter.Handle("/providers/", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS")
apiRouter.Handle("/providers", http.HandlerFunc(han.ListProviders)).Methods("GET", "OPTIONS")
// Websocket log writer
apiRouter.Handle("/{ws:ws\\/?}", log(logWriter, http.HandlerFunc(han.WSHandler))).Methods("GET")
apiRouter.Handle("/{ws:ws\\/?}", http.HandlerFunc(han.WSHandler)).Methods("GET")
return router
}

View file

@ -18,25 +18,11 @@ import (
"context"
"garm/params"
"garm/runner/providers/common"
)
type contextFlags string
/*
// InstanceJWTClaims holds JWT claims
type InstanceJWTClaims struct {
ID string `json:"id"`
Name string `json:"name"`
PoolID string `json:"provider_id"`
// Scope is either repository or organization
Scope common.PoolType `json:"scope"`
// Entity is the repo or org name
Entity string `json:"entity"`
jwt.StandardClaims
}
*/
const (
isAdminKey contextFlags = "is_admin"
fullNameKey contextFlags = "full_name"
@ -45,11 +31,13 @@ const (
isEnabledFlag contextFlags = "is_enabled"
jwtTokenFlag contextFlags = "jwt_token"
instanceIDKey contextFlags = "id"
instanceNameKey contextFlags = "name"
instancePoolIDKey contextFlags = "pool_id"
instancePoolTypeKey contextFlags = "scope"
instanceEntityKey contextFlags = "entity"
instanceIDKey contextFlags = "id"
instanceNameKey contextFlags = "name"
instancePoolIDKey contextFlags = "pool_id"
instancePoolTypeKey contextFlags = "scope"
instanceEntityKey contextFlags = "entity"
instanceRunnerStatus contextFlags = "status"
instanceTokenFetched contextFlags = "tokenFetched"
)
func SetInstanceID(ctx context.Context, id string) context.Context {
@ -64,6 +52,30 @@ func InstanceID(ctx context.Context) string {
return elem.(string)
}
func SetInstanceTokenFetched(ctx context.Context, fetched bool) context.Context {
return context.WithValue(ctx, instanceTokenFetched, fetched)
}
func InstanceTokenFetched(ctx context.Context) bool {
elem := ctx.Value(instanceTokenFetched)
if elem == nil {
return false
}
return elem.(bool)
}
func SetInstanceRunnerStatus(ctx context.Context, val common.RunnerStatus) context.Context {
return context.WithValue(ctx, instanceRunnerStatus, val)
}
func InstanceRunnerStatus(ctx context.Context) common.RunnerStatus {
elem := ctx.Value(instanceRunnerStatus)
if elem == nil {
return common.RunnerPending
}
return elem.(common.RunnerStatus)
}
func SetInstanceName(ctx context.Context, val string) context.Context {
return context.WithValue(ctx, instanceNameKey, val)
}
@ -116,6 +128,8 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont
ctx = SetInstanceID(ctx, instance.ID)
ctx = SetInstanceName(ctx, instance.Name)
ctx = SetInstancePoolID(ctx, instance.PoolID)
ctx = SetInstanceRunnerStatus(ctx, instance.RunnerStatus)
ctx = SetInstanceTokenFetched(ctx, instance.TokenFetched)
return ctx
}

View file

@ -26,6 +26,7 @@ import (
runnerErrors "garm/errors"
"garm/params"
"garm/runner/common"
providerCommon "garm/runner/providers/common"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
@ -145,6 +146,14 @@ func (amw *instanceMiddleware) Middleware(next http.Handler) http.Handler {
if InstanceID(ctx) == "" {
invalidAuthResponse(w)
return
}
runnerStatus := InstanceRunnerStatus(ctx)
if runnerStatus != providerCommon.RunnerInstalling && runnerStatus != providerCommon.RunnerPending {
// Instances that have finished installing can no longer authenticate to the API
invalidAuthResponse(w)
return
}
// ctx = SetJWTClaim(ctx, *claims)

View file

@ -77,8 +77,7 @@ func invalidAuthResponse(w http.ResponseWriter) {
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(
apiParams.APIErrorResponse{
Error: "Authentication failed",
Details: "Invalid authentication token",
Error: "Authentication failed",
})
}

View file

@ -23,15 +23,22 @@ import (
var CloudConfigTemplate = `#!/bin/bash
set -ex
set -e
set -o pipefail
CALLBACK_URL="{{ .CallbackURL }}"
METADATA_URL="{{ .MetadataURL }}"
BEARER_TOKEN="{{ .CallbackToken }}"
if [ -z "$METADATA_URL" ];then
echo "no token is available and METADATA_URL is not set"
exit 1
fi
GITHUB_TOKEN=$(curl --fail -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${METADATA_URL}/runner-registration-token/")
function call() {
PAYLOAD="$1"
curl -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
curl --fail -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
@ -55,13 +62,11 @@ sendStatus "downloading tools from {{ .DownloadURL }}"
TEMP_TOKEN=""
if [ ! -z "{{ .TempDownloadToken }}" ]; then
TEMP_TOKEN="Authorization: Bearer {{ .TempDownloadToken }}"
fi
curl -L -H "${TEMP_TOKEN}" -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools"
curl -L -H "${TEMP_TOKEN}" -o "/home/{{ .RunnerUsername }}/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools"
mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder"
@ -74,7 +79,7 @@ cd /home/{{ .RunnerUsername }}/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "configuring runner"
sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "{{ .GithubToken }}" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner"
sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install {{ .RunnerUsername }} || fail "failed to install service"
@ -98,7 +103,7 @@ type InstallRunnerParams struct {
RunnerUsername string
RunnerGroup string
RepoURL string
GithubToken string
MetadataURL string
RunnerName string
RunnerLabels string
CallbackURL string

View file

@ -20,6 +20,7 @@ import (
"fmt"
"log"
"net"
"net/url"
"os"
"path/filepath"
"time"
@ -169,8 +170,12 @@ type Default 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,omitempty" json:"config-dir,omitempty"`
ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"`
// CallbackURL is the URL where the instances can send back status reports.
CallbackURL string `toml:"callback_url" json:"callback-url"`
// MetadataURL is the URL where instances can fetch information they may need
// to set themselves up.
MetadataURL string `toml:"metadata_url" json:"metadata-url"`
// LogFile is the location of the log file.
LogFile string `toml:"log_file,omitempty" json:"log-file"`
EnableLogStreamer bool `toml:"enable_log_streamer"`
@ -180,6 +185,17 @@ func (d *Default) Validate() error {
if d.CallbackURL == "" {
return fmt.Errorf("missing callback_url")
}
_, err := url.Parse(d.CallbackURL)
if err != nil {
return errors.Wrap(err, "validating callback_url")
}
if d.MetadataURL == "" {
return fmt.Errorf("missing metadata-url")
}
if _, err := url.Parse(d.MetadataURL); err != nil {
return errors.Wrap(err, "validating metadata_url")
}
if d.ConfigDir == "" {
return fmt.Errorf("config_dir cannot be empty")

View file

@ -15,7 +15,6 @@
package config
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -33,6 +32,7 @@ func getDefaultSectionConfig(configDir string) Default {
return Default{
ConfigDir: configDir,
CallbackURL: "https://garm.example.com/",
MetadataURL: "https://garm.example.com/api/v1/metadata",
LogFile: filepath.Join(configDir, "garm.log"),
}
}
@ -105,7 +105,7 @@ func getDefaultJWTCofig() JWTAuth {
}
func getDefaultConfig(t *testing.T) Config {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
@ -129,7 +129,7 @@ func TestConfig(t *testing.T) {
}
func TestDefaultSectionConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
@ -150,14 +150,25 @@ func TestDefaultSectionConfig(t *testing.T) {
name: "CallbackURL cannot be empty",
cfg: Default{
CallbackURL: "",
MetadataURL: cfg.MetadataURL,
ConfigDir: cfg.ConfigDir,
},
errString: "missing callback_url",
},
{
name: "MetadataURL cannot be empty",
cfg: Default{
CallbackURL: cfg.CallbackURL,
MetadataURL: "",
ConfigDir: cfg.ConfigDir,
},
errString: "missing metadata-url",
},
{
name: "ConfigDir cannot be empty",
cfg: Default{
CallbackURL: cfg.CallbackURL,
MetadataURL: cfg.MetadataURL,
ConfigDir: "",
},
errString: "config_dir cannot be empty",
@ -166,6 +177,7 @@ func TestDefaultSectionConfig(t *testing.T) {
name: "config_dir must exist and be accessible",
cfg: Default{
CallbackURL: cfg.CallbackURL,
MetadataURL: cfg.MetadataURL,
ConfigDir: "/i/do/not/exist",
},
errString: "accessing config dir: stat /i/do/not/exist:.*",
@ -306,14 +318,14 @@ func TestAPITLSconfig(t *testing.T) {
}
func TestTLSConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
invalidCert := filepath.Join(dir, "invalid_cert.pem")
err = ioutil.WriteFile(invalidCert, []byte("bogus content"), 0755)
err = os.WriteFile(invalidCert, []byte("bogus content"), 0755)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}
@ -396,7 +408,7 @@ func TestTLSConfig(t *testing.T) {
}
func TestDatabaseConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
@ -503,7 +515,7 @@ func TestDatabaseConfig(t *testing.T) {
}
func TestGormParams(t *testing.T) {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
@ -527,7 +539,7 @@ func TestGormParams(t *testing.T) {
}
func TestSQLiteConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "garm-config-test")
dir, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
@ -676,7 +688,7 @@ func TestNewConfig(t *testing.T) {
}
func TestNewConfigEmptyConfigDir(t *testing.T) {
dirPath, err := ioutil.TempDir("", "garm-config-test")
dirPath, err := os.MkdirTemp("", "garm-config-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}

View file

@ -16,7 +16,6 @@ package config
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -25,13 +24,13 @@ import (
)
func getDefaultExternalConfig(t *testing.T) External {
dir, err := ioutil.TempDir("", "garm-test")
dir, err := os.MkdirTemp("", "garm-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %s", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
err = ioutil.WriteFile(filepath.Join(dir, "garm-external-provider"), []byte{}, 0755)
err = os.WriteFile(filepath.Join(dir, "garm-external-provider"), []byte{}, 0755)
if err != nil {
t.Fatalf("failed to write file: %s", err)
}

View file

@ -1,5 +1,6 @@
[default]
callback_url = "https://garm.example.com/"
metadata_url = "https://garm.example.com/"
config_dir = ""
[apiserver]

View file

@ -1,5 +1,6 @@
[default]
callback_url = "https://garm.example.com/"
metadata_url = "https://garm.example.com/"
config_dir = "./testdata"
[apiserver]

View file

@ -1,20 +1,28 @@
#!/bin/bash
set -ex
set -e
set -o pipefail
METADATA_URL="GARM_METADATA_URL"
CALLBACK_URL="GARM_CALLBACK_URL"
BEARER_TOKEN="GARM_CALLBACK_TOKEN"
DOWNLOAD_URL="GH_DOWNLOAD_URL"
DOWNLOAD_TOKEN="GH_TEMP_DOWNLOAD_TOKEN"
FILENAME="GH_FILENAME"
TARGET_URL="GH_TARGET_URL"
RUNNER_TOKEN="GH_RUNNER_TOKEN"
RUNNER_NAME="GH_RUNNER_NAME"
RUNNER_LABELS="GH_RUNNER_LABELS"
TEMP_TOKEN=""
if [ -z "$METADATA_URL" ];then
echo "no token is available and METADATA_URL is not set"
exit 1
fi
function call() {
PAYLOAD="$1"
curl -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
curl --fail -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
@ -34,10 +42,12 @@ function fail() {
exit 1
}
if [ ! -z "$DOWNLOAD_TOKEN" ]; then
TEMP_TOKEN="Authorization: Bearer $DOWNLOAD_TOKEN"
fi
sendStatus "downloading tools from ${DOWNLOAD_URL}"
curl -L -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools"
curl --fail -L -H "${TEMP_TOKEN}" -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools"
mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder"
@ -49,8 +59,11 @@ sendStatus "installing dependencies"
cd /home/runner/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "fetching runner registration token"
GITHUB_TOKEN=$(curl --fail -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${METADATA_URL}" || fail "failed to get runner registration token")
sendStatus "configuring runner"
sudo -u runner -- ./config.sh --unattended --url "${TARGET_URL}" --token "${RUNNER_TOKEN}" --name "${RUNNER_NAME}" --labels "${RUNNER_LABELS}" --ephemeral || fail "failed to configure runner"
sudo -u runner -- ./config.sh --unattended --url "${TARGET_URL}" --token "${GITHUB_TOKEN}" --name "${RUNNER_NAME}" --labels "${RUNNER_LABELS}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install runner || fail "failed to install service"
@ -65,4 +78,4 @@ if [ $? -ne 0 ];then
fi
set -e
success "runner successfully installed" $AGENT_ID
success "runner successfully installed" $AGENT_ID

View file

@ -74,6 +74,19 @@ function downloadURL() {
echo "${URL}"
}
function tempDownloadToken() {
[ -z "$1" -o -z "$2" ] && return 1
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
TOKEN=$(echo "$INPUT" | jq -c -r --arg OS "$1" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).temp_download_token')
echo "${TOKEN}"
}
function runnerTokenURL() {
METADATA_URL=$(echo "$INPUT" | jq -c -r '."metadata-url"')
checkValNotNull "${METADATA_URL}" "metadata-url" || return $?
echo "${METADATA_URL}/runner-registration-token/"
}
function downloadFilename() {
[ -z "$1" -o -z "$2" ] && return 1
GH_OS="${AZURE_OS_TO_GH_OS_MAP[$1]}"
@ -113,12 +126,6 @@ function repoURL() {
echo "${REPO}"
}
function ghAccessToken() {
TOKEN=$(echo "$INPUT" | jq -c -r '.github_runner_access_token')
checkValNotNull "${TOKEN}" "github_runner_access_token" || return $?
echo "${TOKEN}"
}
function callbackURL() {
CB_URL=$(echo "$INPUT" | jq -c -r '."callback-url"')
checkValNotNull "${CB_URL}" "callback-url" || return $?
@ -177,6 +184,7 @@ function getCloudConfig() {
ARCH=$(requestedArch)
DW_URL=$(downloadURL "${OS_TYPE}" "${ARCH}")
DW_TOKEN=$(tempDownloadToken "${OS_TYPE}" "${ARCH}")
DW_FILENAME=$(downloadFilename "${OS_TYPE}" "${ARCH}")
LABELS=$(labels)
@ -190,8 +198,9 @@ function getCloudConfig() {
-e "s|GH_DOWNLOAD_URL|${DW_URL}|g" \
-e "s|GH_FILENAME|${DW_FILENAME}|g" \
-e "s|GH_TARGET_URL|$(repoURL)|g" \
-e "s|GH_RUNNER_TOKEN|$(ghAccessToken)|g" \
-e "s|GARM_METADATA_URL|$(runnerTokenURL)|g" \
-e "s|GH_RUNNER_NAME|$(instanceName)|g" \
-e "s|GH_TEMP_DOWNLOAD_TOKEN|${DW_TOKEN}|g" \
-e "s|GH_RUNNER_LABELS|${LABELS}|g" > ${TMP_SCRIPT}
AS_B64=$(base64 -w0 ${TMP_SCRIPT})

View file

@ -1,20 +1,28 @@
#!/bin/bash
set -ex
set -e
set -o pipefail
METADATA_URL="GARM_METADATA_URL"
CALLBACK_URL="GARM_CALLBACK_URL"
BEARER_TOKEN="GARM_CALLBACK_TOKEN"
DOWNLOAD_URL="GH_DOWNLOAD_URL"
DOWNLOAD_TOKEN="GH_TEMP_DOWNLOAD_TOKEN"
FILENAME="GH_FILENAME"
TARGET_URL="GH_TARGET_URL"
RUNNER_TOKEN="GH_RUNNER_TOKEN"
RUNNER_NAME="GH_RUNNER_NAME"
RUNNER_LABELS="GH_RUNNER_LABELS"
TEMP_TOKEN=""
if [ -z "$METADATA_URL" ];then
echo "no token is available and METADATA_URL is not set"
exit 1
fi
function call() {
PAYLOAD="$1"
curl -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
curl --fail -s -X POST -d "${PAYLOAD}" -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${CALLBACK_URL}" || echo "failed to call home: exit code ($?)"
}
function sendStatus() {
@ -34,10 +42,12 @@ function fail() {
exit 1
}
if [ ! -z "$DOWNLOAD_TOKEN" ]; then
TEMP_TOKEN="Authorization: Bearer $DOWNLOAD_TOKEN"
fi
sendStatus "downloading tools from ${DOWNLOAD_URL}"
curl -L -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools"
curl --fail -L -H "${TEMP_TOKEN}" -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools"
mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder"
@ -49,8 +59,11 @@ sendStatus "installing dependencies"
cd /home/runner/actions-runner
sudo ./bin/installdependencies.sh || fail "failed to install dependencies"
sendStatus "fetching runner registration token"
GITHUB_TOKEN=$(curl --fail -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${METADATA_URL}" || fail "failed to get runner registration token")
sendStatus "configuring runner"
sudo -u runner -- ./config.sh --unattended --url "${TARGET_URL}" --token "${RUNNER_TOKEN}" --name "${RUNNER_NAME}" --labels "${RUNNER_LABELS}" --ephemeral || fail "failed to configure runner"
sudo -u runner -- ./config.sh --unattended --url "${TARGET_URL}" --token "${GITHUB_TOKEN}" --name "${RUNNER_NAME}" --labels "${RUNNER_LABELS}" --ephemeral || fail "failed to configure runner"
sendStatus "installing runner service"
./svc.sh install runner || fail "failed to install service"
@ -65,4 +78,4 @@ if [ $? -ne 0 ];then
fi
set -e
success "runner successfully installed" $AGENT_ID
success "runner successfully installed" $AGENT_ID

View file

@ -145,6 +145,19 @@ function downloadURL() {
echo "${URL}"
}
function tempDownloadToken() {
[ -z "$1" -o -z "$2" ] && return 1
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
TOKEN=$(echo "$INPUT" | jq -c -r --arg OS "$1" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).temp_download_token')
echo "${TOKEN}"
}
function runnerTokenURL() {
METADATA_URL=$(echo "$INPUT" | jq -c -r '."metadata-url"')
checkValNotNull "${METADATA_URL}" "metadata-url" || return $?
echo "${METADATA_URL}/runner-registration-token/"
}
function downloadFilename() {
[ -z "$1" -o -z "$2" ] && return 1
GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}"
@ -177,12 +190,6 @@ function repoURL() {
echo "${REPO}"
}
function ghAccessToken() {
TOKEN=$(echo "$INPUT" | jq -c -r '.github_runner_access_token')
checkValNotNull "${TOKEN}" "github_runner_access_token" || return $?
echo "${TOKEN}"
}
function callbackURL() {
CB_URL=$(echo "$INPUT" | jq -c -r '."callback-url"')
checkValNotNull "${CB_URL}" "callback-url" || return $?
@ -215,6 +222,7 @@ function getCloudConfig() {
ARCH=$(requestedArch)
DW_URL=$(downloadURL "${OS_TYPE}" "${ARCH}")
DW_TOKEN=$(tempDownloadToken "${OS_TYPE}" "${ARCH}")
DW_FILENAME=$(downloadFilename "${OS_TYPE}" "${ARCH}")
LABELS=$(labels)
@ -228,8 +236,9 @@ function getCloudConfig() {
-e "s|GH_DOWNLOAD_URL|${DW_URL}|g" \
-e "s|GH_FILENAME|${DW_FILENAME}|g" \
-e "s|GH_TARGET_URL|$(repoURL)|g" \
-e "s|GH_RUNNER_TOKEN|$(ghAccessToken)|g" \
-e "s|GARM_METADATA_URL|$(runnerTokenURL)|g" \
-e "s|GH_RUNNER_NAME|$(instanceName)|g" \
-e "s|GH_TEMP_DOWNLOAD_TOKEN|${DW_TOKEN}|g" \
-e "s|GH_RUNNER_LABELS|${LABELS}|g" > ${TMP_SCRIPT}
AS_B64=$(base64 -w0 ${TMP_SCRIPT})
@ -306,7 +315,7 @@ function CreateInstance() {
if [ $? -ne 0 ];then
CODE=$?
# cleanup
rm -f "${CC_FILE}" || true
rm -f "${CC_FILE}" || true
openstack server delete "${INSTANCE_NAME}" || true
openstack volume delete "${INSTANCE_NAME}" || true
set -e

View file

@ -106,7 +106,8 @@ type InstanceStore interface {
ListAllInstances(ctx context.Context) ([]params.Instance, error)
GetInstanceByName(ctx context.Context, instanceName string) (params.Instance, error)
AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error
AddInstanceEvent(ctx context.Context, instanceID string, event params.EventType, eventLevel params.EventLevel, eventMessage string) error
ListInstanceEvents(ctx context.Context, instanceID string, eventType params.EventType, eventLevel params.EventLevel) ([]params.StatusMessage, error)
}
//go:generate mockery --name=Store

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks
@ -14,13 +14,13 @@ type Store struct {
mock.Mock
}
// AddInstanceStatusMessage provides a mock function with given fields: ctx, instanceID, statusMessage
func (_m *Store) AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error {
ret := _m.Called(ctx, instanceID, statusMessage)
// AddInstanceEvent provides a mock function with given fields: ctx, instanceID, event, statusMessage
func (_m *Store) AddInstanceEvent(ctx context.Context, instanceID string, event params.EventType, statusMessage string) error {
ret := _m.Called(ctx, instanceID, event, statusMessage)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, instanceID, statusMessage)
if rf, ok := ret.Get(0).(func(context.Context, string, params.EventType, string) error); ok {
r0 = rf(ctx, instanceID, event, statusMessage)
} else {
r0 = ret.Error(0)
}

View file

@ -30,6 +30,7 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching pool")
}
newInstance := Instance{
Pool: pool,
Name: param.Name,
@ -38,6 +39,7 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p
OSType: param.OSType,
OSArch: param.OSArch,
CallbackURL: param.CallbackURL,
MetadataURL: param.MetadataURL,
}
q := s.conn.Create(&newInstance)
if q.Error != nil {
@ -112,6 +114,7 @@ func (s *sqlDatabase) GetPoolInstanceByName(ctx context.Context, poolID string,
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
return s.sqlToParamsInstance(instance), nil
}
@ -120,6 +123,7 @@ func (s *sqlDatabase) GetInstanceByName(ctx context.Context, instanceName string
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
return s.sqlToParamsInstance(instance), nil
}
@ -137,14 +141,42 @@ func (s *sqlDatabase) DeleteInstance(ctx context.Context, poolID string, instanc
return nil
}
func (s *sqlDatabase) AddInstanceStatusMessage(ctx context.Context, instanceID string, statusMessage string) error {
func (s *sqlDatabase) ListInstanceEvents(ctx context.Context, instanceID string, eventType params.EventType, eventLevel params.EventLevel) ([]params.StatusMessage, error) {
var events []InstanceStatusUpdate
query := s.conn.Model(&InstanceStatusUpdate{}).Where("instance_id = ?", instanceID)
if eventLevel != "" {
query = query.Where("event_level = ?", eventLevel)
}
if eventType != "" {
query = query.Where("event_type = ?", eventType)
}
if result := query.Find(&events); result.Error != nil {
return nil, errors.Wrap(result.Error, "fetching events")
}
eventParams := make([]params.StatusMessage, len(events))
for idx, val := range events {
eventParams[idx] = params.StatusMessage{
Message: val.Message,
EventType: val.EventType,
EventLevel: val.EventLevel,
}
}
return eventParams, nil
}
func (s *sqlDatabase) AddInstanceEvent(ctx context.Context, instanceID string, event params.EventType, eventLevel params.EventLevel, statusMessage string) error {
instance, err := s.getInstanceByID(ctx, instanceID)
if err != nil {
return errors.Wrap(err, "updating instance")
}
msg := InstanceStatusUpdate{
Message: statusMessage,
Message: statusMessage,
EventType: event,
EventLevel: eventLevel,
}
if err := s.conn.Model(&instance).Association("StatusMessages").Append(&msg); err != nil {
@ -186,6 +218,10 @@ func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, par
instance.CreateAttempt = param.CreateAttempt
}
if param.TokenFetched != nil {
instance.TokenFetched = *param.TokenFetched
}
instance.ProviderFault = param.ProviderFault
q := s.conn.Save(&instance)
@ -205,17 +241,25 @@ func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, par
return params.Instance{}, errors.Wrap(err, "updating addresses")
}
}
return s.sqlToParamsInstance(instance), nil
}
func (s *sqlDatabase) ListPoolInstances(ctx context.Context, poolID string) ([]params.Instance, error) {
pool, err := s.getPoolByID(ctx, poolID, "Tags", "Instances")
u, err := uuid.FromString(poolID)
if err != nil {
return nil, errors.Wrap(err, "fetching pool")
return nil, errors.Wrap(runnerErrors.ErrBadRequest, "parsing id")
}
ret := make([]params.Instance, len(pool.Instances))
for idx, inst := range pool.Instances {
var instances []Instance
query := s.conn.Model(&Instance{}).Where("pool_id = ?", u)
if err := query.Find(&instances); err.Error != nil {
return nil, errors.Wrap(err.Error, "fetching instances")
}
ret := make([]params.Instance, len(instances))
for idx, inst := range instances {
ret[idx] = s.sqlToParamsInstance(inst)
}
return ret, nil

View file

@ -343,11 +343,11 @@ func (s *InstancesTestSuite) TestDeleteInstanceDBDeleteErr() {
s.Require().Equal("deleting instance: mocked delete instance error", err.Error())
}
func (s *InstancesTestSuite) TestAddInstanceStatusMessage() {
func (s *InstancesTestSuite) TestAddInstanceEvent() {
storeInstance := s.Fixtures.Instances[0]
statusMsg := "test-status-message"
err := s.Store.AddInstanceStatusMessage(context.Background(), storeInstance.ID, statusMsg)
err := s.Store.AddInstanceEvent(context.Background(), storeInstance.ID, params.StatusEvent, params.EventInfo, statusMsg)
s.Require().Nil(err)
instance, err := s.Store.GetInstanceByName(context.Background(), storeInstance.Name)
@ -358,13 +358,13 @@ func (s *InstancesTestSuite) TestAddInstanceStatusMessage() {
s.Require().Equal(statusMsg, instance.StatusMessages[0].Message)
}
func (s *InstancesTestSuite) TestAddInstanceStatusMessageInvalidPoolID() {
err := s.Store.AddInstanceStatusMessage(context.Background(), "dummy-id", "dummy-message")
func (s *InstancesTestSuite) TestAddInstanceEventInvalidPoolID() {
err := s.Store.AddInstanceEvent(context.Background(), "dummy-id", params.StatusEvent, params.EventInfo, "dummy-message")
s.Require().Equal("updating instance: parsing id: invalid request", err.Error())
}
func (s *InstancesTestSuite) TestAddInstanceStatusMessageDBUpdateErr() {
func (s *InstancesTestSuite) TestAddInstanceEventDBUpdateErr() {
instance := s.Fixtures.Instances[0]
statusMsg := "test-status-message"
@ -390,7 +390,7 @@ func (s *InstancesTestSuite) TestAddInstanceStatusMessageDBUpdateErr() {
WillReturnError(fmt.Errorf("mocked add status message error"))
s.Fixtures.SQLMock.ExpectRollback()
err := s.StoreSQLMocked.AddInstanceStatusMessage(context.Background(), instance.ID, statusMsg)
err := s.StoreSQLMocked.AddInstanceEvent(context.Background(), instance.ID, params.StatusEvent, params.EventInfo, statusMsg)
s.assertSQLMockExpectations()
s.Require().NotNil(err)
@ -496,7 +496,7 @@ func (s *InstancesTestSuite) TestListPoolInstances() {
func (s *InstancesTestSuite) TestListPoolInstancesInvalidPoolID() {
_, err := s.Store.ListPoolInstances(context.Background(), "dummy-pool-id")
s.Require().Equal("fetching pool: parsing id: invalid request", err.Error())
s.Require().Equal("parsing id: invalid request", err.Error())
}
func (s *InstancesTestSuite) TestListAllInstances() {

View file

@ -16,6 +16,7 @@ package sql
import (
"garm/config"
"garm/params"
"garm/runner/providers/common"
"time"
@ -118,7 +119,9 @@ type Address struct {
type InstanceStatusUpdate struct {
Base
Message string `gorm:"type:text"`
EventType params.EventType `gorm:"index:eventType"`
EventLevel params.EventLevel
Message string `gorm:"type:text"`
InstanceID uuid.UUID
Instance Instance `gorm:"foreignKey:InstanceID"`
@ -138,8 +141,10 @@ type Instance struct {
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
ProviderFault []byte `gorm:"type:longblob"`
CreateAttempt int
TokenFetched bool
PoolID uuid.UUID
Pool Pool `gorm:"foreignKey:PoolID"`

View file

@ -51,28 +51,6 @@ type OrgTestSuite struct {
Fixtures *OrgTestFixtures
}
func (s *OrgTestSuite) equalOrgsByName(expected, actual []params.Organization) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].Name > expected[j].Name })
sort.Slice(actual, func(i, j int) bool { return actual[i].Name > actual[j].Name })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].Name, actual[i].Name)
}
}
func (s *OrgTestSuite) equalPoolsByID(expected, actual []params.Pool) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *OrgTestSuite) equalInstancesByName(expected, actual []params.Instance) {
s.Require().Equal(len(expected), len(actual))
@ -276,7 +254,7 @@ func (s *OrgTestSuite) TestListOrganizations() {
orgs, err := s.Store.ListOrganizations(context.Background())
s.Require().Nil(err)
s.equalOrgsByName(s.Fixtures.Orgs, orgs)
garmTesting.EqualDBEntityByName(s.T(), s.Fixtures.Orgs, orgs)
}
func (s *OrgTestSuite) TestListOrganizationsDBFetchErr() {
@ -689,7 +667,7 @@ func (s *OrgTestSuite) TestListOrgPools() {
pools, err := s.Store.ListOrgPools(context.Background(), s.Fixtures.Orgs[0].ID)
s.Require().Nil(err)
s.equalPoolsByID(orgPools, pools)
garmTesting.EqualDBEntityID(s.T(), orgPools, pools)
}
func (s *OrgTestSuite) TestListOrgPoolsInvalidOrgID() {

View file

@ -22,7 +22,6 @@ import (
garmTesting "garm/internal/testing"
"garm/params"
"regexp"
"sort"
"testing"
"github.com/stretchr/testify/suite"
@ -45,17 +44,6 @@ type PoolsTestSuite struct {
Fixtures *PoolsTestFixtures
}
func (s *PoolsTestSuite) equalPoolsByID(expected, actual []params.Pool) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *PoolsTestSuite) assertSQLMockExpectations() {
err := s.Fixtures.SQLMock.ExpectationsWereMet()
if err != nil {
@ -134,7 +122,7 @@ func (s *PoolsTestSuite) TestListAllPools() {
pools, err := s.Store.ListAllPools(context.Background())
s.Require().Nil(err)
s.equalPoolsByID(s.Fixtures.Pools, pools)
garmTesting.EqualDBEntityID(s.T(), s.Fixtures.Pools, pools)
}
func (s *PoolsTestSuite) TestListAllPoolsDBFetchErr() {

View file

@ -60,17 +60,6 @@ func (s *RepoTestSuite) equalReposByName(expected, actual []params.Repository) {
}
}
func (s *RepoTestSuite) equalPoolsByID(expected, actual []params.Pool) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *RepoTestSuite) equalInstancesByID(expected, actual []params.Instance) {
s.Require().Equal(len(expected), len(actual))
@ -714,7 +703,7 @@ func (s *RepoTestSuite) TestListRepoPools() {
pools, err := s.Store.ListRepoPools(context.Background(), s.Fixtures.Repos[0].ID)
s.Require().Nil(err)
s.equalPoolsByID(repoPools, pools)
garmTesting.EqualDBEntityID(s.T(), repoPools, pools)
}
func (s *RepoTestSuite) TestListRepoPoolsInvalidRepoID() {

View file

@ -41,9 +41,11 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance {
RunnerStatus: instance.RunnerStatus,
PoolID: instance.PoolID.String(),
CallbackURL: instance.CallbackURL,
MetadataURL: instance.MetadataURL,
StatusMessages: []params.StatusMessage{},
CreateAttempt: instance.CreateAttempt,
UpdatedAt: instance.UpdatedAt,
TokenFetched: instance.TokenFetched,
}
if len(instance.ProviderFault) > 0 {
@ -56,8 +58,10 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance {
for _, msg := range instance.StatusMessages {
ret.StatusMessages = append(ret.StatusMessages, params.StatusMessage{
CreatedAt: msg.CreatedAt,
Message: msg.Message,
CreatedAt: msg.CreatedAt,
Message: msg.Message,
EventType: msg.EventType,
EventLevel: msg.EventLevel,
})
}
return ret

View file

@ -81,7 +81,7 @@ Global Flags:
Use "garm-cli completion [command] --help" for more information about a command.
```
## Adding a repository/organization
## Adding a repository/organization/enterprise
To add a repository, we need credentials. Let's list the available credentials currently configured. These credentials are added to ```garm``` using the config file (see above), but we need to reference them by name when creating a repo.
@ -128,6 +128,22 @@ ubuntu@experiments:~$ garm-cli organization create \
+-------------+--------------------------------------+
```
To add an enterprise, use the following command:
```bash
ubuntu@experiments:~$ garm-cli enterprise create \
--credentials=gabriel \
--name=gsamfira \
--webhook-secret="$SECRET"
+-------------+--------------------------------------+
| FIELD | VALUE |
+-------------+--------------------------------------+
| ID | 0925033b-049f-4334-a460-c26f979d2356 |
| Name | gsamfira |
| Credentials | gabriel |
+-------------+--------------------------------------+
```
## Creating a pool
Pools are objects that define one type of worker and rules by which that pool of workers will be maintained. You can have multiple pools of different types of instances. Each pool can have different images, be on different providers and have different tags.
@ -356,18 +372,22 @@ Usage:
Available Commands:
completion Generate the autocompletion script for the specified shell
credentials List configured credentials
debug-log Stream garm log
enterprise Manage enterprise
help Help about any command
init Initialize a newly installed garm
login Log into a manager
organization Manage organizations
pool List pools
profile Add, delete or update profiles
provider Interacts with the providers API resource.
repository Manage repositories
runner List runners in a pool
version Print version and exit
Flags:
--debug Enable debug on all API calls
-h, --help help for garm-cli
Use "garm-cli [command] --help" for more information about a command.
```

View file

@ -68,7 +68,7 @@ garm-cli runner show garm-f5227755-129d-4e2d-b306-377a8f3a5dfe
+-----------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
```
This URL if set, must be accessible by the instance. If you wish to restrict access to it, a reverse proxy can be configured to accept requests only from networks in which the runners ```garm``` manages will be spun up. This URL doesn't need to be globally accessible, it just needs to be accessible by the instances.
This URL must be set and must be accessible by the instance. If you wish to restrict access to it, a reverse proxy can be configured to accept requests only from networks in which the runners ```garm``` manages will be spun up. This URL doesn't need to be globally accessible, it just needs to be accessible by the instances.
For example, in a scenario where you expose the API endpoint directly, this setting could look like the following:
@ -76,6 +76,20 @@ For example, in a scenario where you expose the API endpoint directly, this sett
callback_url = "https://garm.example.com/api/v1/callbacks/status"
```
Authentication is done using a short-lived JWT token, that gets generated for a particular instance that we are spinning up. That JWT token only has access to update it's own status. No other API endpoints will work with that JWT token. The validity of the token is equal to the pool bootstrap timeout value (default 20 minutes) plus the garm polling interval (5 minutes).
Authentication is done using a short-lived JWT token, that gets generated for a particular instance that we are spinning up. That JWT token grants access to the instance to only update it's own status and to fetch metadata for itself. No other API endpoints will work with that JWT token. The validity of the token is equal to the pool bootstrap timeout value (default 20 minutes) plus the garm polling interval (5 minutes).
There is a sample ```nginx``` config [in the testdata folder](/testdata/nginx-server.conf). Feel free to customize it whichever way you see fit.
There is a sample ```nginx``` config [in the testdata folder](/testdata/nginx-server.conf). Feel free to customize it whichever way you see fit.
## The metadata_url option
The metadata URL is the base URL for any information an instance may need to fetch in order to finish setting itself up. As this URL may be placed behind a reverse proxy, you'll need to configure it in the ```garm``` config file. Ultimately this URL will need to point to the following ```garm``` API endpoint:
```bash
GET /api/v1/metadata
```
This URL needs to be accessible only by the instances ```garm``` sets up. This URL will not be used by anyone else. To configure it in ```garm``` add the following line in the ```[default]``` section of your ```garm``` config:
```toml
metadata_url = "https://garm.example.com/api/v1/metadata"
```

View file

@ -21,7 +21,10 @@ import (
"garm/config"
"os"
"path/filepath"
"sort"
"testing"
"github.com/stretchr/testify/require"
)
var (
@ -44,3 +47,42 @@ func GetTestSqliteDBConfig(t *testing.T) config.Database {
},
}
}
type IDDBEntity interface {
GetID() string
}
type NameAndIDDBEntity interface {
IDDBEntity
GetName() string
}
func EqualDBEntityByName[T NameAndIDDBEntity](t *testing.T, expected, actual []T) {
require.Equal(t, len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].GetName() > expected[j].GetName() })
sort.Slice(actual, func(i, j int) bool { return actual[i].GetName() > actual[j].GetName() })
for i := 0; i < len(expected); i++ {
require.Equal(t, expected[i].GetName(), actual[i].GetName())
}
}
func EqualDBEntityID[T IDDBEntity](t *testing.T, expected, actual []T) {
require.Equal(t, len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].GetID() > expected[j].GetID() })
sort.Slice(actual, func(i, j int) bool { return actual[i].GetID() > actual[j].GetID() })
for i := 0; i < len(expected); i++ {
require.Equal(t, expected[i].GetID(), actual[i].GetID())
}
}
func DBEntityMapToSlice[T NameAndIDDBEntity](orgs map[string]T) []T {
orgsSlice := []T{}
for _, value := range orgs {
orgsSlice = append(orgsSlice, value)
}
return orgsSlice
}

View file

@ -24,20 +24,35 @@ import (
)
type AddressType string
type EventType string
type EventLevel string
const (
PublicAddress AddressType = "public"
PrivateAddress AddressType = "private"
)
const (
StatusEvent EventType = "status"
FetchTokenEvent EventType = "fetchToken"
)
const (
EventInfo EventLevel = "info"
EventWarning EventLevel = "warning"
EventError EventLevel = "error"
)
type Address struct {
Address string `json:"address"`
Type AddressType `json:"type"`
}
type StatusMessage struct {
CreatedAt time.Time `json:"created_at"`
Message string `json:"message"`
CreatedAt time.Time `json:"created_at"`
Message string `json:"message"`
EventType EventType `json:"event_type"`
EventLevel EventLevel `json:"event_level"`
}
type Instance struct {
@ -73,11 +88,21 @@ type Instance struct {
ProviderFault []byte `json:"provider_fault,omitempty"`
StatusMessages []StatusMessage `json:"status_messages,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
// Do not serialize sensitive info.
CallbackURL string `json:"-"`
CreateAttempt int `json:"-"`
UpdatedAt time.Time `json:"updated_at"`
CallbackURL string `json:"-"`
MetadataURL string `json:"-"`
CreateAttempt int `json:"-"`
TokenFetched bool `json:"-"`
}
func (i Instance) GetName() string {
return i.Name
}
func (i Instance) GetID() string {
return i.ID
}
type BootstrapInstance struct {
@ -85,12 +110,11 @@ type BootstrapInstance struct {
Tools []*github.RunnerApplicationDownload `json:"tools"`
// RepoURL is the URL the github runner agent needs to configure itself.
RepoURL string `json:"repo_url"`
// GithubRunnerAccessToken is the token we fetch from github to allow the runner to
// register itself.
GithubRunnerAccessToken string `json:"github_runner_access_token"`
// CallbackUrl is the URL where the instance can send a post, signaling
// progress or status.
CallbackURL string `json:"callback-url"`
// MetadataURL is the URL where instances can fetch information needed to set themselves up.
MetadataURL string `json:"metadata-url"`
// InstanceToken is the token that needs to be set by the instance in the headers
// in order to send updated back to the garm via CallbackURL.
InstanceToken string `json:"instance-token"`
@ -133,6 +157,10 @@ type Pool struct {
RunnerBootstrapTimeout uint `json:"runner_bootstrap_timeout"`
}
func (p Pool) GetID() string {
return p.ID
}
func (p *Pool) RunnerTimeout() uint {
if p.RunnerBootstrapTimeout == 0 {
return config.DefaultRunnerBootstrapTimeout
@ -144,6 +172,7 @@ type Internal struct {
OAuth2Token string `json:"oauth2"`
ControllerID string `json:"controller_id"`
InstanceCallbackURL string `json:"instance_callback_url"`
InstanceMetadataURL string `json:"instance_metadata_url"`
JWTSecret string `json:"jwt_secret"`
// GithubCredentialsDetails contains all info about the credentials, except the
// token, which is added above.
@ -161,6 +190,14 @@ type Repository struct {
WebhookSecret string `json:"-"`
}
func (r Repository) GetName() string {
return r.Name
}
func (r Repository) GetID() string {
return r.ID
}
type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
@ -171,6 +208,14 @@ type Organization struct {
WebhookSecret string `json:"-"`
}
func (o Organization) GetName() string {
return o.Name
}
func (o Organization) GetID() string {
return o.ID
}
type Enterprise struct {
ID string `json:"id"`
Name string `json:"name"`
@ -181,6 +226,14 @@ type Enterprise struct {
WebhookSecret string `json:"-"`
}
func (e Enterprise) GetName() string {
return e.Name
}
func (e Enterprise) GetID() string {
return e.ID
}
// Users holds information about a particular user
type User struct {
ID string `json:"id"`
@ -227,3 +280,8 @@ type PoolManagerStatus struct {
IsRunning bool `json:"running"`
FailureReason string `json:"failure_reason,omitempty"`
}
type RunnerInfo struct {
Name string
Labels []string
}

View file

@ -74,7 +74,7 @@ type CreateEnterpriseParams struct {
func (c *CreateEnterpriseParams) Validate() error {
if c.Name == "" {
return errors.NewBadRequestError("missing org name")
return errors.NewBadRequestError("missing enterprise name")
}
if c.CredentialsName == "" {
@ -113,6 +113,7 @@ type CreateInstanceParams struct {
Status common.InstanceStatus
RunnerStatus common.RunnerStatus
CallbackURL string
MetadataURL string
CreateAttempt int `json:"-"`
}
@ -172,6 +173,7 @@ type UpdateInstanceParams struct {
ProviderFault []byte `json:"provider_fault,omitempty"`
AgentID int64 `json:"-"`
CreateAttempt int `json:"-"`
TokenFetched *bool `json:"-"`
}
type UpdateUserParams struct {

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks
@ -27,6 +27,27 @@ func (_m *PoolManager) ForceDeleteRunner(runner params.Instance) error {
return r0
}
// GithubRunnerRegistrationToken provides a mock function with given fields:
func (_m *PoolManager) GithubRunnerRegistrationToken() (string, error) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HandleWorkflowJob provides a mock function with given fields: job
func (_m *PoolManager) HandleWorkflowJob(job params.WorkflowJob) error {
ret := _m.Called(job)

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks

View file

@ -43,6 +43,7 @@ const (
type PoolManager interface {
ID() string
WebhookSecret() string
GithubRunnerRegistrationToken() (string, error)
HandleWorkflowJob(job params.WorkflowJob) error
RefreshState(param params.UpdatePoolStateParams) error
ForceDeleteRunner(runner params.Instance) error

548
runner/enterprises_test.go Normal file
View file

@ -0,0 +1,548 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package runner
import (
"context"
"fmt"
"garm/auth"
"garm/config"
"garm/database"
dbCommon "garm/database/common"
runnerErrors "garm/errors"
garmTesting "garm/internal/testing"
"garm/params"
"garm/runner/common"
runnerCommonMocks "garm/runner/common/mocks"
runnerMocks "garm/runner/mocks"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type EnterpriseTestFixtures struct {
AdminContext context.Context
DBFile string
Store dbCommon.Store
StoreEnterprises map[string]params.Enterprise
Providers map[string]common.Provider
Credentials map[string]config.Github
CreateEnterpriseParams params.CreateEnterpriseParams
CreatePoolParams params.CreatePoolParams
CreateInstanceParams params.CreateInstanceParams
UpdateRepoParams params.UpdateRepositoryParams
UpdatePoolParams params.UpdatePoolParams
UpdatePoolStateParams params.UpdatePoolStateParams
ErrMock error
ProviderMock *runnerCommonMocks.Provider
PoolMgrMock *runnerCommonMocks.PoolManager
PoolMgrCtrlMock *runnerMocks.PoolManagerController
}
type EnterpriseTestSuite struct {
suite.Suite
Fixtures *EnterpriseTestFixtures
Runner *Runner
}
func (s *EnterpriseTestSuite) SetupTest() {
adminCtx := auth.GetAdminContext()
// create testing sqlite database
dbCfg := garmTesting.GetTestSqliteDBConfig(s.T())
db, err := database.NewDatabase(adminCtx, dbCfg)
if err != nil {
s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
}
// create some organization objects in the database, for testing purposes
enterprises := map[string]params.Enterprise{}
for i := 1; i <= 3; i++ {
name := fmt.Sprintf("test-enterprise-%v", i)
enterprise, err := db.CreateEnterprise(
adminCtx,
name,
fmt.Sprintf("test-creds-%v", i),
fmt.Sprintf("test-webhook-secret-%v", i),
)
if err != nil {
s.FailNow(fmt.Sprintf("failed to create database object (test-enterprise-%v)", i))
}
enterprises[name] = enterprise
}
// setup test fixtures
var maxRunners uint = 40
var minIdleRunners uint = 20
providerMock := runnerCommonMocks.NewProvider(s.T())
fixtures := &EnterpriseTestFixtures{
AdminContext: adminCtx,
DBFile: dbCfg.SQLite.DBFile,
Store: db,
StoreEnterprises: enterprises,
Providers: map[string]common.Provider{
"test-provider": providerMock,
},
Credentials: map[string]config.Github{
"test-creds": {
Name: "test-creds-name",
Description: "test-creds-description",
OAuth2Token: "test-creds-oauth2-token",
},
},
CreateEnterpriseParams: params.CreateEnterpriseParams{
Name: "test-enterprise-create",
CredentialsName: "test-creds",
},
CreatePoolParams: params.CreatePoolParams{
ProviderName: "test-provider",
MaxRunners: 4,
MinIdleRunners: 2,
Image: "test",
Flavor: "test",
OSType: "linux",
OSArch: "arm64",
Tags: []string{"self-hosted", "arm64", "linux"},
RunnerBootstrapTimeout: 0,
},
CreateInstanceParams: params.CreateInstanceParams{
Name: "test-instance-name",
OSType: "linux",
},
UpdateRepoParams: params.UpdateRepositoryParams{
CredentialsName: "test-creds",
WebhookSecret: "test-update-repo-webhook-secret",
},
UpdatePoolParams: params.UpdatePoolParams{
MaxRunners: &maxRunners,
MinIdleRunners: &minIdleRunners,
Image: "test-images-updated",
Flavor: "test-flavor-updated",
},
UpdatePoolStateParams: params.UpdatePoolStateParams{
WebhookSecret: "test-update-repo-webhook-secret",
},
ErrMock: fmt.Errorf("mock error"),
ProviderMock: providerMock,
PoolMgrMock: runnerCommonMocks.NewPoolManager(s.T()),
PoolMgrCtrlMock: runnerMocks.NewPoolManagerController(s.T()),
}
s.Fixtures = fixtures
// setup test runner
runner := &Runner{
providers: fixtures.Providers,
credentials: fixtures.Credentials,
ctx: fixtures.AdminContext,
store: fixtures.Store,
poolManagerCtrl: fixtures.PoolMgrCtrlMock,
}
s.Runner = runner
}
func (s *EnterpriseTestSuite) TestCreateEnterprise() {
// setup mocks expectations
s.Fixtures.PoolMgrMock.On("Start").Return(nil)
s.Fixtures.PoolMgrCtrlMock.On("CreateEnterprisePoolManager", s.Fixtures.AdminContext, mock.AnythingOfType("params.Enterprise"), s.Fixtures.Providers, s.Fixtures.Store).Return(s.Fixtures.PoolMgrMock, nil)
// call tested function
enterprise, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams)
// assertions
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Nil(err)
s.Require().Equal(s.Fixtures.CreateEnterpriseParams.Name, enterprise.Name)
s.Require().Equal(s.Fixtures.Credentials[s.Fixtures.CreateEnterpriseParams.CredentialsName].Name, enterprise.CredentialsName)
}
func (s *EnterpriseTestSuite) TestCreateEnterpriseErrUnauthorized() {
_, err := s.Runner.CreateEnterprise(context.Background(), s.Fixtures.CreateEnterpriseParams)
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestCreateEnterpriseEmptyParams() {
_, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, params.CreateEnterpriseParams{})
s.Require().Regexp("validating params: missing enterprise name", err.Error())
}
func (s *EnterpriseTestSuite) TestCreateEnterpriseMissingCredentials() {
s.Fixtures.CreateEnterpriseParams.CredentialsName = "not-existent-creds-name"
_, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams)
s.Require().Equal(runnerErrors.NewBadRequestError("credentials %s not defined", s.Fixtures.CreateEnterpriseParams.CredentialsName), err)
}
func (s *EnterpriseTestSuite) TestCreateEnterpriseAlreadyExists() {
s.Fixtures.CreateEnterpriseParams.Name = "test-enterprise-1" // this is already created in `SetupTest()`
_, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams)
s.Require().Equal(runnerErrors.NewConflictError("enterprise %s already exists", s.Fixtures.CreateEnterpriseParams.Name), err)
}
func (s *EnterpriseTestSuite) TestCreateEnterprisePoolMgrFailed() {
s.Fixtures.PoolMgrCtrlMock.On("CreateEnterprisePoolManager", s.Fixtures.AdminContext, mock.AnythingOfType("params.Enterprise"), s.Fixtures.Providers, s.Fixtures.Store).Return(s.Fixtures.PoolMgrMock, s.Fixtures.ErrMock)
_, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(fmt.Sprintf("creating enterprise pool manager: %s", s.Fixtures.ErrMock.Error()), err.Error())
}
func (s *EnterpriseTestSuite) TestCreateEnterpriseStartPoolMgrFailed() {
s.Fixtures.PoolMgrMock.On("Start").Return(s.Fixtures.ErrMock)
s.Fixtures.PoolMgrCtrlMock.On("CreateEnterprisePoolManager", s.Fixtures.AdminContext, mock.AnythingOfType("params.Enterprise"), s.Fixtures.Providers, s.Fixtures.Store).Return(s.Fixtures.PoolMgrMock, nil)
s.Fixtures.PoolMgrCtrlMock.On("DeleteEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.ErrMock)
_, err := s.Runner.CreateEnterprise(s.Fixtures.AdminContext, s.Fixtures.CreateEnterpriseParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(fmt.Sprintf("starting enterprise pool manager: %s", s.Fixtures.ErrMock.Error()), err.Error())
}
func (s *EnterpriseTestSuite) TestListEnterprises() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil)
orgs, err := s.Runner.ListEnterprises(s.Fixtures.AdminContext)
s.Require().Nil(err)
garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreEnterprises), orgs)
}
func (s *EnterpriseTestSuite) TestListEnterprisesErrUnauthorized() {
_, err := s.Runner.ListEnterprises(context.Background())
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestGetEnterpriseByID() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
s.Fixtures.PoolMgrMock.On("Status").Return(params.PoolManagerStatus{IsRunning: true}, nil)
org, err := s.Runner.GetEnterpriseByID(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
s.Require().Nil(err)
s.Require().Equal(s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, org.ID)
}
func (s *EnterpriseTestSuite) TestGetEnterpriseByIDErrUnauthorized() {
_, err := s.Runner.GetEnterpriseByID(context.Background(), "dummy-org-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestDeleteEnterprise() {
s.Fixtures.PoolMgrCtrlMock.On("DeleteEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(nil)
err := s.Runner.DeleteEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-3"].ID)
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Nil(err)
_, err = s.Fixtures.Store.GetEnterpriseByID(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-3"].ID)
s.Require().Equal("fetching enterprise: not found", err.Error())
}
func (s *EnterpriseTestSuite) TestDeleteEnterpriseErrUnauthorized() {
err := s.Runner.DeleteEnterprise(context.Background(), "dummy-enterprise-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolDefinedFailed() {
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create store enterprises pool: %v", err))
}
err = s.Runner.DeleteEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
s.Require().Equal(runnerErrors.NewBadRequestError("enterprise has pools defined (%s)", pool.ID), err)
}
func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolMgrFailed() {
s.Fixtures.PoolMgrCtrlMock.On("DeleteEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.ErrMock)
err := s.Runner.DeleteEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(fmt.Sprintf("deleting enterprise pool manager: %s", s.Fixtures.ErrMock.Error()), err.Error())
}
func (s *EnterpriseTestSuite) TestUpdateEnterprise() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
s.Fixtures.PoolMgrCtrlMock.On("CreateEnterprisePoolManager", s.Fixtures.AdminContext, mock.AnythingOfType("params.Enterprise"), s.Fixtures.Providers, s.Fixtures.Store).Return(s.Fixtures.PoolMgrMock, nil)
org, err := s.Runner.UpdateEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.UpdateRepoParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Nil(err)
s.Require().Equal(s.Fixtures.UpdateRepoParams.CredentialsName, org.CredentialsName)
s.Require().Equal(s.Fixtures.UpdateRepoParams.WebhookSecret, org.WebhookSecret)
}
func (s *EnterpriseTestSuite) TestUpdateEnterpriseErrUnauthorized() {
_, err := s.Runner.UpdateEnterprise(context.Background(), "dummy-enterprise-id", s.Fixtures.UpdateRepoParams)
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestUpdateEnterpriseInvalidCreds() {
s.Fixtures.UpdateRepoParams.CredentialsName = "invalid-creds-name"
_, err := s.Runner.UpdateEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.UpdateRepoParams)
s.Require().Equal(runnerErrors.NewBadRequestError("invalid credentials (%s) for enterprise %s", s.Fixtures.UpdateRepoParams.CredentialsName, s.Fixtures.StoreEnterprises["test-enterprise-1"].Name), err)
}
func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolMgrFailed() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, s.Fixtures.ErrMock)
s.Fixtures.PoolMgrMock.On("RefreshState", s.Fixtures.UpdatePoolStateParams).Return(s.Fixtures.ErrMock)
_, err := s.Runner.UpdateEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.UpdateRepoParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(fmt.Sprintf("updating enterprise pool manager: %s", s.Fixtures.ErrMock.Error()), err.Error())
}
func (s *EnterpriseTestSuite) TestUpdateEnterpriseCreateEnterprisePoolMgrFailed() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
s.Fixtures.PoolMgrCtrlMock.On("CreateEnterprisePoolManager", s.Fixtures.AdminContext, mock.AnythingOfType("params.Enterprise"), s.Fixtures.Providers, s.Fixtures.Store).Return(s.Fixtures.PoolMgrMock, s.Fixtures.ErrMock)
_, err := s.Runner.UpdateEnterprise(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.UpdateRepoParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(fmt.Sprintf("creating enterprise pool manager: %s", s.Fixtures.ErrMock.Error()), err.Error())
}
func (s *EnterpriseTestSuite) TestCreateEnterprisePool() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
pool, err := s.Runner.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Nil(err)
enterprise, err := s.Fixtures.Store.GetEnterpriseByID(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
if err != nil {
s.FailNow(fmt.Sprintf("cannot get enterprise by ID: %v", err))
}
s.Require().Equal(1, len(enterprise.Pools))
s.Require().Equal(pool.ID, enterprise.Pools[0].ID)
s.Require().Equal(s.Fixtures.CreatePoolParams.ProviderName, enterprise.Pools[0].ProviderName)
s.Require().Equal(s.Fixtures.CreatePoolParams.MaxRunners, enterprise.Pools[0].MaxRunners)
s.Require().Equal(s.Fixtures.CreatePoolParams.MinIdleRunners, enterprise.Pools[0].MinIdleRunners)
}
func (s *EnterpriseTestSuite) TestCreateEnterprisePoolErrUnauthorized() {
_, err := s.Runner.CreateEnterprisePool(context.Background(), "dummy-enterprise-id", s.Fixtures.CreatePoolParams)
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestCreateEnterprisePoolErrNotFound() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, runnerErrors.ErrNotFound)
_, err := s.Runner.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Equal(runnerErrors.ErrNotFound, err)
}
func (s *EnterpriseTestSuite) TestCreateEnterprisePoolFetchPoolParamsFailed() {
s.Fixtures.CreatePoolParams.ProviderName = "not-existent-provider-name"
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
_, err := s.Runner.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Regexp("fetching pool params: no such provider", err.Error())
}
func (s *EnterpriseTestSuite) TestGetEnterprisePoolByID() {
enterprisePool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %s", err))
}
pool, err := s.Runner.GetEnterprisePoolByID(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, enterprisePool.ID)
s.Require().Nil(err)
s.Require().Equal(enterprisePool.ID, pool.ID)
}
func (s *EnterpriseTestSuite) TestGetEnterprisePoolByIDErrUnauthorized() {
_, err := s.Runner.GetEnterprisePoolByID(context.Background(), "dummy-enterprise-id", "dummy-pool-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestDeleteEnterprisePool() {
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %s", err))
}
err = s.Runner.DeleteEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, pool.ID)
s.Require().Nil(err)
_, err = s.Fixtures.Store.GetEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, pool.ID)
s.Require().Equal("fetching pool: not found", err.Error())
}
func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolErrUnauthorized() {
err := s.Runner.DeleteEnterprisePool(context.Background(), "dummy-enterprise-id", "dummy-pool-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestDeleteEnterprisePoolRunnersFailed() {
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err))
}
instance, err := s.Fixtures.Store.CreateInstance(s.Fixtures.AdminContext, pool.ID, s.Fixtures.CreateInstanceParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create instance: %s", err))
}
err = s.Runner.DeleteEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, pool.ID)
s.Require().Equal(runnerErrors.NewBadRequestError("pool has runners: %s", instance.ID), err)
}
func (s *EnterpriseTestSuite) TestListEnterprisePools() {
enterprisePools := []params.Pool{}
for i := 1; i <= 2; i++ {
s.Fixtures.CreatePoolParams.Image = fmt.Sprintf("test-enterprise-%v", i)
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create org pool: %v", err))
}
enterprisePools = append(enterprisePools, pool)
}
pools, err := s.Runner.ListEnterprisePools(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
s.Require().Nil(err)
garmTesting.EqualDBEntityID(s.T(), enterprisePools, pools)
}
func (s *EnterpriseTestSuite) TestListOrgPoolsErrUnauthorized() {
_, err := s.Runner.ListOrgPools(context.Background(), "dummy-org-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestUpdateEnterprisePool() {
enterprisePool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %s", err))
}
pool, err := s.Runner.UpdateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, enterprisePool.ID, s.Fixtures.UpdatePoolParams)
s.Require().Nil(err)
s.Require().Equal(*s.Fixtures.UpdatePoolParams.MaxRunners, pool.MaxRunners)
s.Require().Equal(*s.Fixtures.UpdatePoolParams.MinIdleRunners, pool.MinIdleRunners)
}
func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolErrUnauthorized() {
_, err := s.Runner.UpdateEnterprisePool(context.Background(), "dummy-enterprise-id", "dummy-pool-id", s.Fixtures.UpdatePoolParams)
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestUpdateEnterprisePoolMinIdleGreaterThanMax() {
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %s", err))
}
var maxRunners uint = 10
var minIdleRunners uint = 11
s.Fixtures.UpdatePoolParams.MaxRunners = &maxRunners
s.Fixtures.UpdatePoolParams.MinIdleRunners = &minIdleRunners
_, err = s.Runner.UpdateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, pool.ID, s.Fixtures.UpdatePoolParams)
s.Require().Equal(runnerErrors.NewBadRequestError("min_idle_runners cannot be larger than max_runners"), err)
}
func (s *EnterpriseTestSuite) TestListEnterpriseInstances() {
pool, err := s.Fixtures.Store.CreateEnterprisePool(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID, s.Fixtures.CreatePoolParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create enterprise pool: %v", err))
}
poolInstances := []params.Instance{}
for i := 1; i <= 3; i++ {
s.Fixtures.CreateInstanceParams.Name = fmt.Sprintf("test-enterprise-%v", i)
instance, err := s.Fixtures.Store.CreateInstance(s.Fixtures.AdminContext, pool.ID, s.Fixtures.CreateInstanceParams)
if err != nil {
s.FailNow(fmt.Sprintf("cannot create instance: %s", err))
}
poolInstances = append(poolInstances, instance)
}
instances, err := s.Runner.ListEnterpriseInstances(s.Fixtures.AdminContext, s.Fixtures.StoreEnterprises["test-enterprise-1"].ID)
s.Require().Nil(err)
garmTesting.EqualDBEntityID(s.T(), poolInstances, instances)
}
func (s *EnterpriseTestSuite) TestListEnterpriseInstancesErrUnauthorized() {
_, err := s.Runner.ListEnterpriseInstances(context.Background(), "dummy-enterprise-id")
s.Require().Equal(runnerErrors.ErrUnauthorized, err)
}
func (s *EnterpriseTestSuite) TestFindEnterprisePoolManager() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, nil)
poolManager, err := s.Runner.findEnterprisePoolManager(s.Fixtures.StoreEnterprises["test-enterprise-1"].Name)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Nil(err)
s.Require().Equal(s.Fixtures.PoolMgrMock, poolManager)
}
func (s *EnterpriseTestSuite) TestFindEnterprisePoolManagerFetchPoolMgrFailed() {
s.Fixtures.PoolMgrCtrlMock.On("GetEnterprisePoolManager", mock.AnythingOfType("params.Enterprise")).Return(s.Fixtures.PoolMgrMock, s.Fixtures.ErrMock)
_, err := s.Runner.findEnterprisePoolManager(s.Fixtures.StoreEnterprises["test-enterprise-1"].Name)
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
s.Require().Regexp("fetching pool manager for enterprise", err.Error())
}
func TestEnterpriseTestSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(EnterpriseTestSuite))
}

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
// Code generated by mockery v2.15.0. DO NOT EDIT.
package mocks

View file

@ -27,7 +27,6 @@ import (
"garm/runner/common"
runnerCommonMocks "garm/runner/common/mocks"
runnerMocks "garm/runner/mocks"
"sort"
"testing"
"github.com/stretchr/testify/mock"
@ -59,47 +58,6 @@ type OrgTestSuite struct {
Runner *Runner
}
func (s *OrgTestSuite) orgsMapValues(orgs map[string]params.Organization) []params.Organization {
orgsSlice := []params.Organization{}
for _, value := range orgs {
orgsSlice = append(orgsSlice, value)
}
return orgsSlice
}
func (s *OrgTestSuite) equalOrgsByName(expected, actual []params.Organization) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].Name > expected[j].Name })
sort.Slice(actual, func(i, j int) bool { return actual[i].Name > actual[j].Name })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].Name, actual[i].Name)
}
}
func (s *OrgTestSuite) equalPoolsByID(expected, actual []params.Pool) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *OrgTestSuite) equalInstancesByName(expected, actual []params.Instance) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].Name > expected[j].Name })
sort.Slice(actual, func(i, j int) bool { return actual[i].Name > actual[j].Name })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].Name, actual[i].Name)
}
}
func (s *OrgTestSuite) SetupTest() {
adminCtx := auth.GetAdminContext()
@ -267,7 +225,7 @@ func (s *OrgTestSuite) TestListOrganizations() {
orgs, err := s.Runner.ListOrganizations(s.Fixtures.AdminContext)
s.Require().Nil(err)
s.equalOrgsByName(s.orgsMapValues(s.Fixtures.StoreOrgs), orgs)
garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreOrgs), orgs)
}
func (s *OrgTestSuite) TestListOrganizationsErrUnauthorized() {
@ -493,7 +451,7 @@ func (s *OrgTestSuite) TestListOrgPools() {
pools, err := s.Runner.ListOrgPools(s.Fixtures.AdminContext, s.Fixtures.StoreOrgs["test-org-1"].ID)
s.Require().Nil(err)
s.equalPoolsByID(orgPools, pools)
garmTesting.EqualDBEntityID(s.T(), orgPools, pools)
}
func (s *OrgTestSuite) TestListOrgPoolsErrUnauthorized() {
@ -554,7 +512,7 @@ func (s *OrgTestSuite) TestListOrgInstances() {
instances, err := s.Runner.ListOrgInstances(s.Fixtures.AdminContext, s.Fixtures.StoreOrgs["test-org-1"].ID)
s.Require().Nil(err)
s.equalInstancesByName(poolInstances, instances)
garmTesting.EqualDBEntityID(s.T(), poolInstances, instances)
}
func (s *OrgTestSuite) TestListOrgInstancesErrUnauthorized() {
@ -585,5 +543,6 @@ func (s *OrgTestSuite) TestFindOrgPoolManagerFetchPoolMgrFailed() {
}
func TestOrgTestSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(OrgTestSuite))
}

View file

@ -18,7 +18,7 @@ import (
)
// test that we implement PoolManager
var _ poolHelper = &organization{}
var _ poolHelper = &enterprise{}
func NewEnterprisePoolManager(ctx context.Context, cfg params.Enterprise, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) {
ghc, ghEnterpriseClient, err := util.GithubClient(ctx, cfgInternal.OAuth2Token, cfgInternal.GithubCredentialsDetails)
@ -61,18 +61,25 @@ type enterprise struct {
mux sync.Mutex
}
func (r *enterprise) GetRunnerNameFromWorkflow(job params.WorkflowJob) (string, error) {
func (r *enterprise) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) {
if err := r.ValidateOwner(job); err != nil {
return params.RunnerInfo{}, errors.Wrap(err, "validating owner")
}
workflow, ghResp, err := r.ghcli.GetWorkflowJobByID(r.ctx, job.Repository.Owner.Login, job.Repository.Name, job.WorkflowJob.ID)
if err != nil {
if ghResp.StatusCode == http.StatusUnauthorized {
return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners")
return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info")
}
return "", errors.Wrap(err, "fetching workflow info")
return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info")
}
if workflow.RunnerName != nil {
return *workflow.RunnerName, nil
return params.RunnerInfo{
Name: *workflow.RunnerName,
Labels: workflow.Labels,
}, nil
}
return "", fmt.Errorf("failed to find runner name from workflow")
return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow")
}
func (r *enterprise) UpdateState(param params.UpdatePoolStateParams) error {
@ -179,6 +186,10 @@ func (r *enterprise) GetCallbackURL() string {
return r.cfgInternal.InstanceCallbackURL
}
func (r *enterprise) GetMetadataURL() string {
return r.cfgInternal.InstanceMetadataURL
}
func (r *enterprise) FindPoolByTags(labels []string) (params.Pool, error) {
pool, err := r.store.FindEnterprisePoolByTags(r.ctx, r.id, labels)
if err != nil {

View file

@ -24,7 +24,7 @@ type poolHelper interface {
GetGithubToken() string
GetGithubRunners() ([]*github.Runner, error)
GetGithubRegistrationToken() (string, error)
GetRunnerNameFromWorkflow(job params.WorkflowJob) (string, error)
GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error)
RemoveGithubRunner(runnerID int64) (*github.Response, error)
FetchTools() ([]*github.RunnerApplicationDownload, error)
@ -34,6 +34,7 @@ type poolHelper interface {
JwtToken() string
String() string
GetCallbackURL() string
GetMetadataURL() string
FindPoolByTags(labels []string) (params.Pool, error)
GetPoolByID(poolID string) (params.Pool, error)
ValidateOwner(job params.WorkflowJob) error

View file

@ -73,18 +73,25 @@ type organization struct {
mux sync.Mutex
}
func (r *organization) GetRunnerNameFromWorkflow(job params.WorkflowJob) (string, error) {
func (r *organization) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) {
if err := r.ValidateOwner(job); err != nil {
return params.RunnerInfo{}, errors.Wrap(err, "validating owner")
}
workflow, ghResp, err := r.ghcli.GetWorkflowJobByID(r.ctx, job.Organization.Login, job.Repository.Name, job.WorkflowJob.ID)
if err != nil {
if ghResp.StatusCode == http.StatusUnauthorized {
return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runner name")
return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info")
}
return "", errors.Wrap(err, "fetching workflow info")
return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info")
}
if workflow.RunnerName != nil {
return *workflow.RunnerName, nil
return params.RunnerInfo{
Name: *workflow.RunnerName,
Labels: workflow.Labels,
}, nil
}
return "", fmt.Errorf("failed to find runner name from workflow")
return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow")
}
func (r *organization) UpdateState(param params.UpdatePoolStateParams) error {
@ -192,6 +199,10 @@ func (r *organization) GetCallbackURL() string {
return r.cfgInternal.InstanceCallbackURL
}
func (r *organization) GetMetadataURL() string {
return r.cfgInternal.InstanceMetadataURL
}
func (r *organization) FindPoolByTags(labels []string) (params.Pool, error) {
pool, err := r.store.FindOrganizationPoolByTags(r.ctx, r.id, labels)
if err != nil {

View file

@ -67,16 +67,37 @@ type basePoolManager struct {
mux sync.Mutex
}
func controllerIDFromLabels(labels []*github.RunnerLabels) string {
func controllerIDFromLabels(labels []string) string {
for _, lbl := range labels {
if lbl.Name != nil && strings.HasPrefix(*lbl.Name, controllerLabelPrefix) {
labelName := *lbl.Name
return labelName[len(controllerLabelPrefix):]
if strings.HasPrefix(lbl, controllerLabelPrefix) {
return lbl[len(controllerLabelPrefix):]
}
}
return ""
}
func labelsFromRunner(runner *github.Runner) []string {
if runner == nil || runner.Labels == nil {
return []string{}
}
var labels []string
for _, val := range runner.Labels {
if val == nil {
continue
}
labels = append(labels, val.GetName())
}
return labels
}
// isManagedRunner returns true if labels indicate the runner belongs to a pool
// this manager is responsible for.
func (r *basePoolManager) isManagedRunner(labels []string) bool {
runnerControllerID := controllerIDFromLabels(labels)
return runnerControllerID == r.controllerID
}
// cleanupOrphanedProviderRunners compares runners in github with local runners and removes
// any local runners that are not present in Github. Runners that are "idle" in our
// provider, but do not exist in github, will be removed. This can happen if the
@ -92,6 +113,10 @@ func (r *basePoolManager) cleanupOrphanedProviderRunners(runners []*github.Runne
runnerNames := map[string]bool{}
for _, run := range runners {
if !r.isManagedRunner(labelsFromRunner(run)) {
log.Printf("runner %s is not managed by a pool belonging to %s", *run.Name, r.helper.String())
continue
}
runnerNames[*run.Name] = true
}
@ -127,24 +152,26 @@ func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error {
runnerNames := map[string]bool{}
for _, run := range runners {
if !r.isManagedRunner(labelsFromRunner(run)) {
log.Printf("runner %s is not managed by a pool belonging to %s", *run.Name, r.helper.String())
continue
}
runnerNames[*run.Name] = true
}
for _, instance := range dbInstances {
if ok := runnerNames[instance.Name]; !ok {
if instance.Status == providerCommon.InstanceRunning {
pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID)
if err != nil {
return errors.Wrap(err, "fetching instance pool info")
}
if time.Since(instance.UpdatedAt).Minutes() < float64(pool.RunnerTimeout()) {
continue
}
log.Printf("reaping instance %s due to timeout", instance.Name)
if err := r.setInstanceStatus(instance.Name, providerCommon.InstancePendingDelete, nil); err != nil {
log.Printf("failed to update runner %s status", instance.Name)
return errors.Wrap(err, "updating runner")
}
pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID)
if err != nil {
return errors.Wrap(err, "fetching instance pool info")
}
if time.Since(instance.UpdatedAt).Minutes() < float64(pool.RunnerTimeout()) {
continue
}
log.Printf("reaping instance %s due to timeout", instance.Name)
if err := r.setInstanceStatus(instance.Name, providerCommon.InstancePendingDelete, nil); err != nil {
log.Printf("failed to update runner %s status", instance.Name)
return errors.Wrap(err, "updating runner")
}
}
}
@ -157,9 +184,8 @@ func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error {
// first remove the instance from github, and then from our database.
func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner) error {
for _, runner := range runners {
runnerControllerID := controllerIDFromLabels(runner.Labels)
if runnerControllerID != r.controllerID {
// Not a runner we manage. Do not remove foreign runner.
if !r.isManagedRunner(labelsFromRunner(runner)) {
log.Printf("runner %s is not managed by a pool belonging to %s", *runner.Name, r.helper.String())
continue
}
@ -265,6 +291,11 @@ func (r *basePoolManager) fetchInstance(runnerName string) (params.Instance, err
return params.Instance{}, errors.Wrap(err, "fetching instance")
}
_, err = r.helper.GetPoolByID(runner.PoolID)
if err != nil {
return params.Instance{}, errors.Wrap(err, "fetching pool")
}
return runner, nil
}
@ -312,6 +343,9 @@ func (r *basePoolManager) acquireNewInstance(job params.WorkflowJob) error {
pool, err := r.helper.FindPoolByTags(requestedLabels)
if err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
return 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)
@ -353,6 +387,7 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string) error {
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackURL: r.helper.GetCallbackURL(),
MetadataURL: r.helper.GetMetadataURL(),
CreateAttempt: 1,
}
@ -512,16 +547,6 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
labels = append(labels, r.controllerLabel())
labels = append(labels, r.poolLabel(pool.ID))
tk, err := r.helper.GetGithubRegistrationToken()
if err != nil {
if errors.Is(err, runnerErrors.ErrUnauthorized) {
failureReason := fmt.Sprintf("failed to fetch registration token: %q", err)
r.setPoolRunningState(false, failureReason)
log.Print(failureReason)
}
return errors.Wrap(err, "fetching registration token")
}
jwtValidity := pool.RunnerTimeout()
var poolType common.PoolType = common.RepositoryPool
if pool.OrgID != "" {
@ -535,18 +560,18 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
}
bootstrapArgs := params.BootstrapInstance{
Name: instance.Name,
Tools: r.tools,
RepoURL: r.helper.GithubURL(),
GithubRunnerAccessToken: tk,
CallbackURL: instance.CallbackURL,
InstanceToken: jwtToken,
OSArch: pool.OSArch,
Flavor: pool.Flavor,
Image: pool.Image,
Labels: labels,
PoolID: instance.PoolID,
CACertBundle: r.credsDetails.CABundle,
Name: instance.Name,
Tools: r.tools,
RepoURL: r.helper.GithubURL(),
MetadataURL: instance.MetadataURL,
CallbackURL: instance.CallbackURL,
InstanceToken: jwtToken,
OSArch: pool.OSArch,
Flavor: pool.Flavor,
Image: pool.Image,
Labels: labels,
PoolID: instance.PoolID,
CACertBundle: r.credsDetails.CABundle,
}
var instanceIDToDelete string
@ -581,25 +606,39 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
return nil
}
func (r *basePoolManager) getRunnerNameFromJob(job params.WorkflowJob) (string, error) {
if job.WorkflowJob.RunnerName != "" {
return job.WorkflowJob.RunnerName, nil
func (r *basePoolManager) getRunnerDetailsFromJob(job params.WorkflowJob) (params.RunnerInfo, error) {
runnerInfo := params.RunnerInfo{
Name: job.WorkflowJob.RunnerName,
Labels: job.WorkflowJob.Labels,
}
// Runner name was not set in WorkflowJob by github. We can still attempt to
// fetch the info we need, using the workflow run ID, from the API.
log.Printf("runner name not found in workflow job, attempting to fetch from API")
runnerName, err := r.helper.GetRunnerNameFromWorkflow(job)
if err != nil {
if errors.Is(err, runnerErrors.ErrUnauthorized) {
failureReason := fmt.Sprintf("failed to fetch runner name from API: %q", err)
r.setPoolRunningState(false, failureReason)
log.Print(failureReason)
var err error
if job.WorkflowJob.RunnerName == "" {
// Runner name was not set in WorkflowJob by github. We can still attempt to
// fetch the info we need, using the workflow run ID, from the API.
log.Printf("runner name not found in workflow job, attempting to fetch from API")
runnerInfo, err = r.helper.GetRunnerInfoFromWorkflow(job)
if err != nil {
if errors.Is(err, runnerErrors.ErrUnauthorized) {
failureReason := fmt.Sprintf("failed to fetch runner name from API: %q", err)
r.setPoolRunningState(false, failureReason)
log.Print(failureReason)
}
return params.RunnerInfo{}, errors.Wrap(err, "fetching runner name from API")
}
return "", errors.Wrap(err, "fetching runner name from API")
}
return runnerName, nil
runnerDetails, err := r.store.GetInstanceByName(context.Background(), runnerInfo.Name)
if err != nil {
log.Printf("could not find runner details for %s", runnerInfo.Name)
return params.RunnerInfo{}, errors.Wrap(err, "fetching runner details")
}
if _, err := r.helper.GetPoolByID(runnerDetails.PoolID); err != nil {
log.Printf("runner %s (pool ID: %s) does not belong to any pool we manage: %s", runnerDetails.Name, runnerDetails.PoolID, err)
return params.RunnerInfo{}, errors.Wrap(err, "fetching pool for instance")
}
return runnerInfo, nil
}
func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error {
@ -621,34 +660,52 @@ func (r *basePoolManager) HandleWorkflowJob(job params.WorkflowJob) error {
case "completed":
// ignore the error here. A completed job may not have a runner name set
// if it was never assigned to a runner, and was canceled.
runnerName, _ := r.getRunnerNameFromJob(job)
// Set instance in database to pending delete.
if runnerName == "" {
runnerInfo, err := r.getRunnerDetailsFromJob(job)
if err != nil {
// Unassigned jobs will have an empty runner_name.
// There is nothing to to in this case.
log.Printf("no runner was assigned. Skipping.")
// We also need to ignore not found errors, as we may get a webhook regarding
// a workflow that is handled by a runner at a different hierarchy level.
return nil
}
// update instance workload state.
if err := r.setInstanceRunnerStatus(runnerName, providerCommon.RunnerTerminated); err != nil {
log.Printf("failed to update runner %s status", runnerName)
if err := r.setInstanceRunnerStatus(runnerInfo.Name, providerCommon.RunnerTerminated); err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
return nil
}
log.Printf("failed to update runner %s status", runnerInfo.Name)
return errors.Wrap(err, "updating runner")
}
log.Printf("marking instance %s as pending_delete", runnerName)
if err := r.setInstanceStatus(runnerName, providerCommon.InstancePendingDelete, nil); err != nil {
log.Printf("failed to update runner %s status", runnerName)
log.Printf("marking instance %s as pending_delete", runnerInfo.Name)
if err := r.setInstanceStatus(runnerInfo.Name, providerCommon.InstancePendingDelete, nil); err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
return nil
}
log.Printf("failed to update runner %s status", runnerInfo.Name)
return errors.Wrap(err, "updating runner")
}
case "in_progress":
// in_progress jobs must have a runner name/ID assigned. Sometimes github will send a hook without
// a runner set. In such cases, we attemt to fetch it from the API.
runnerName, err := r.getRunnerNameFromJob(job)
runnerInfo, err := r.getRunnerDetailsFromJob(job)
if err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
// This is most likely a runner we're not managing. If we define a repo from within an org
// and also define that same org, we will get a hook from github from both the repo and the org
// regarding the same workflow. We look for the runner in the database, and make sure it exists and is
// part of a pool that this manager is responsible for. A not found error here will most likely mean
// that we are not responsible for that runner, and we should ignore it.
return nil
}
return errors.Wrap(err, "determining runner name")
}
// update instance workload state.
if err := r.setInstanceRunnerStatus(runnerName, providerCommon.RunnerActive); err != nil {
log.Printf("failed to update runner %s status", job.WorkflowJob.RunnerName)
if err := r.setInstanceRunnerStatus(runnerInfo.Name, providerCommon.RunnerActive); err != nil {
if errors.Is(err, runnerErrors.ErrNotFound) {
return nil
}
log.Printf("failed to update runner %s status", runnerInfo.Name)
return errors.Wrap(err, "updating runner")
}
}
@ -725,10 +782,11 @@ func (r *basePoolManager) retryFailedInstancesForOnePool(pool params.Pool) {
existingInstances, err := r.store.ListPoolInstances(r.ctx, pool.ID)
if err != nil {
log.Printf("failed to ensure minimum idle workers for pool %s: %s", pool.ID, err)
log.Printf("retrying failed instances: failed to list instances for pool %s: %s", pool.ID, err)
return
}
wg := sync.WaitGroup{}
for _, instance := range existingInstances {
if instance.Status != providerCommon.InstanceError {
continue
@ -736,28 +794,33 @@ func (r *basePoolManager) retryFailedInstancesForOnePool(pool params.Pool) {
if instance.CreateAttempt >= maxCreateAttempts {
continue
}
wg.Add(1)
go func(inst params.Instance) {
defer wg.Done()
// NOTE(gabriel-samfira): this is done in parallel. If there are many failed instances
// this has the potential to create many API requests to the target provider.
// TODO(gabriel-samfira): implement request throttling.
if err := r.deleteInstanceFromProvider(inst); err != nil {
log.Printf("failed to delete instance %s from provider: %s", inst.Name, err)
}
// NOTE(gabriel-samfira): this is done in parallel. If there are many failed instances
// this has the potential to create many API requests to the target provider.
// TODO(gabriel-samfira): implement request throttling.
if instance.ProviderID == "" && instance.Name == "" {
// This really should not happen, but no harm in being extra paranoid. The name is set
// when creating a db entity for the runner, so we should at least have a name.
return
}
// TODO(gabriel-samfira): Incrementing CreateAttempt should be done within a transaction.
// It's fairly safe to do here (for now), as there should be no other code path that updates
// an instance in this state.
updateParams := params.UpdateInstanceParams{
CreateAttempt: instance.CreateAttempt + 1,
Status: providerCommon.InstancePendingCreate,
}
log.Printf("queueing previously failed instance %s for retry", instance.Name)
// Set instance to pending create and wait for retry.
if err := r.updateInstance(instance.Name, updateParams); err != nil {
log.Printf("failed to update runner %s status", instance.Name)
}
// TODO(gabriel-samfira): Incrementing CreateAttempt should be done within a transaction.
// It's fairly safe to do here (for now), as there should be no other code path that updates
// an instance in this state.
var tokenFetched bool = false
updateParams := params.UpdateInstanceParams{
CreateAttempt: inst.CreateAttempt + 1,
TokenFetched: &tokenFetched,
Status: providerCommon.InstancePendingCreate,
}
log.Printf("queueing previously failed instance %s for retry", inst.Name)
// Set instance to pending create and wait for retry.
if err := r.updateInstance(inst.Name, updateParams); err != nil {
log.Printf("failed to update runner %s status", inst.Name)
}
}(instance)
}
wg.Wait()
}
func (r *basePoolManager) retryFailedInstances() {
@ -816,9 +879,6 @@ func (r *basePoolManager) deleteInstanceFromProvider(instance params.Instance) e
return errors.Wrap(err, "removing instance")
}
if err := r.store.DeleteInstance(r.ctx, pool.ID, instance.Name); err != nil {
return errors.Wrap(err, "deleting instance from database")
}
return nil
}
@ -855,6 +915,10 @@ func (r *basePoolManager) deletePendingInstances() {
if err != nil {
log.Printf("failed to delete instance from provider: %+v", err)
}
if err := r.store.DeleteInstance(r.ctx, instance.PoolID, instance.Name); err != nil {
return errors.Wrap(err, "deleting instance from database")
}
return
}(instance)
}
@ -969,6 +1033,10 @@ func (r *basePoolManager) WebhookSecret() string {
return r.helper.WebhookSecret()
}
func (r *basePoolManager) GithubRunnerRegistrationToken() (string, error) {
return r.helper.GetGithubRegistrationToken()
}
func (r *basePoolManager) ID() string {
return r.helper.ID()
}

View file

@ -75,18 +75,25 @@ type repository struct {
mux sync.Mutex
}
func (r *repository) GetRunnerNameFromWorkflow(job params.WorkflowJob) (string, error) {
func (r *repository) GetRunnerInfoFromWorkflow(job params.WorkflowJob) (params.RunnerInfo, error) {
if err := r.ValidateOwner(job); err != nil {
return params.RunnerInfo{}, errors.Wrap(err, "validating owner")
}
workflow, ghResp, err := r.ghcli.GetWorkflowJobByID(r.ctx, job.Repository.Owner.Login, job.Repository.Name, job.WorkflowJob.ID)
if err != nil {
if ghResp.StatusCode == http.StatusUnauthorized {
return "", errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runner name")
return params.RunnerInfo{}, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching workflow info")
}
return "", errors.Wrap(err, "fetching workflow info")
return params.RunnerInfo{}, errors.Wrap(err, "fetching workflow info")
}
if workflow.RunnerName != nil {
return *workflow.RunnerName, nil
return params.RunnerInfo{
Name: *workflow.RunnerName,
Labels: workflow.Labels,
}, nil
}
return "", fmt.Errorf("failed to find runner name from workflow")
return params.RunnerInfo{}, fmt.Errorf("failed to find runner name from workflow")
}
func (r *repository) UpdateState(param params.UpdatePoolStateParams) error {
@ -193,6 +200,10 @@ func (r *repository) GetCallbackURL() string {
return r.cfgInternal.InstanceCallbackURL
}
func (r *repository) GetMetadataURL() string {
return r.cfgInternal.InstanceMetadataURL
}
func (r *repository) FindPoolByTags(labels []string) (params.Pool, error) {
pool, err := r.store.FindRepositoryPoolByTags(r.ctx, r.id, labels)
if err != nil {

View file

@ -29,7 +29,6 @@ func bootstrapParamsToEnv(param params.BootstrapInstance) []string {
fmt.Sprintf("%s_BOOTSTRAP_CALLBACK_URL='%s'", envPrefix, param.CallbackURL),
fmt.Sprintf("%s_BOOTSTRAP_REPO_URL='%s'", envPrefix, param.RepoURL),
fmt.Sprintf("%s_BOOTSTRAP_LABELS='%s'", envPrefix, strings.Join(param.Labels, ",")),
fmt.Sprintf("%s_BOOTSTRAP_GITHUB_ACCESS_TOKEN='%s'", envPrefix, param.GithubRunnerAccessToken),
}
for idx, tool := range param.Tools {

View file

@ -27,7 +27,6 @@ import (
"garm/runner/common"
runnerCommonMocks "garm/runner/common/mocks"
runnerMocks "garm/runner/mocks"
"sort"
"testing"
"github.com/stretchr/testify/mock"
@ -58,47 +57,6 @@ type RepoTestSuite struct {
Runner *Runner
}
func (s *RepoTestSuite) reposMapValues(repos map[string]params.Repository) []params.Repository {
reposSlice := []params.Repository{}
for _, value := range repos {
reposSlice = append(reposSlice, value)
}
return reposSlice
}
func (s *RepoTestSuite) equalReposByName(expected, actual []params.Repository) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].Name > expected[j].Name })
sort.Slice(actual, func(i, j int) bool { return actual[i].Name > actual[j].Name })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].Name, actual[i].Name)
}
}
func (s *RepoTestSuite) equalPoolsByID(expected, actual []params.Pool) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *RepoTestSuite) equalInstancesByID(expected, actual []params.Instance) {
s.Require().Equal(len(expected), len(actual))
sort.Slice(expected, func(i, j int) bool { return expected[i].ID > expected[j].ID })
sort.Slice(actual, func(i, j int) bool { return actual[i].ID > actual[j].ID })
for i := 0; i < len(expected); i++ {
s.Require().Equal(expected[i].ID, actual[i].ID)
}
}
func (s *RepoTestSuite) SetupTest() {
adminCtx := auth.GetAdminContext()
@ -270,7 +228,7 @@ func (s *RepoTestSuite) TestListRepositories() {
repos, err := s.Runner.ListRepositories(s.Fixtures.AdminContext)
s.Require().Nil(err)
s.equalReposByName(s.reposMapValues(s.Fixtures.StoreRepos), repos)
garmTesting.EqualDBEntityByName(s.T(), garmTesting.DBEntityMapToSlice(s.Fixtures.StoreRepos), repos)
}
func (s *RepoTestSuite) TestListRepositoriesErrUnauthorized() {
@ -495,7 +453,7 @@ func (s *RepoTestSuite) TestListRepoPools() {
pools, err := s.Runner.ListRepoPools(s.Fixtures.AdminContext, s.Fixtures.StoreRepos["test-repo-1"].ID)
s.Require().Nil(err)
s.equalPoolsByID(repoPools, pools)
garmTesting.EqualDBEntityID(s.T(), repoPools, pools)
}
func (s *RepoTestSuite) TestListRepoPoolsErrUnauthorized() {
@ -522,7 +480,7 @@ func (s *RepoTestSuite) TestListPoolInstances() {
instances, err := s.Runner.ListPoolInstances(s.Fixtures.AdminContext, pool.ID)
s.Require().Nil(err)
s.equalInstancesByID(poolInstances, instances)
garmTesting.EqualDBEntityID(s.T(), poolInstances, instances)
}
func (s *RepoTestSuite) TestListPoolInstancesErrUnauthorized() {
@ -583,7 +541,7 @@ func (s *RepoTestSuite) TestListRepoInstances() {
instances, err := s.Runner.ListRepoInstances(s.Fixtures.AdminContext, s.Fixtures.StoreRepos["test-repo-1"].ID)
s.Require().Nil(err)
s.equalInstancesByID(poolInstances, instances)
garmTesting.EqualDBEntityID(s.T(), poolInstances, instances)
}
func (s *RepoTestSuite) TestListRepoInstancesErrUnauthorized() {
@ -614,5 +572,6 @@ func (s *RepoTestSuite) TestFindRepoPoolManagerFetchPoolMgrFailed() {
}
func TestRepoTestSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(RepoTestSuite))
}

View file

@ -239,6 +239,7 @@ func (p *poolManagerCtrl) getInternalConfig(credsName string) (params.Internal,
OAuth2Token: creds.OAuth2Token,
ControllerID: p.controllerID,
InstanceCallbackURL: p.config.Default.CallbackURL,
InstanceMetadataURL: p.config.Default.MetadataURL,
JWTSecret: p.config.JWTAuth.Secret,
GithubCredentialsDetails: params.GithubCredentials{
Name: creds.Name,
@ -583,8 +584,10 @@ func (r *Runner) DispatchWorkflowJob(hookTargetType, signature string, jobData [
switch HookTargetType(hookTargetType) {
case RepoHook:
log.Printf("got hook for repo %s/%s", job.Repository.Owner.Login, job.Repository.Name)
poolManager, err = r.findRepoPoolManager(job.Repository.Owner.Login, job.Repository.Name)
case OrganizationHook:
log.Printf("got hook for org %s", job.Organization.Login)
poolManager, err = r.findOrgPoolManager(job.Organization.Login)
case EnterpriseHook:
poolManager, err = r.findEnterprisePoolManager(job.Enterprise.Slug)
@ -703,7 +706,7 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst
return runnerErrors.ErrUnauthorized
}
if err := r.store.AddInstanceStatusMessage(ctx, instanceID, param.Message); err != nil {
if err := r.store.AddInstanceEvent(ctx, instanceID, params.StatusEvent, params.EventInfo, param.Message); err != nil {
return errors.Wrap(err, "adding status update")
}
@ -722,6 +725,95 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst
return nil
}
func (r *Runner) GetInstanceGithubRegistrationToken(ctx context.Context) (string, error) {
instanceName := auth.InstanceName(ctx)
if instanceName == "" {
return "", runnerErrors.ErrUnauthorized
}
// Check if this instance already fetched a registration token. We only allow an instance to
// fetch one token. If the instance fails to bootstrap after a token is fetched, we reset the
// token fetched field when re-queueing the instance.
if auth.InstanceTokenFetched(ctx) {
return "", runnerErrors.ErrUnauthorized
}
status := auth.InstanceRunnerStatus(ctx)
if status != providerCommon.RunnerPending && status != providerCommon.RunnerInstalling {
return "", runnerErrors.ErrUnauthorized
}
instance, err := r.store.GetInstanceByName(ctx, instanceName)
if err != nil {
return "", errors.Wrap(err, "fetching instance")
}
poolMgr, err := r.getPoolManagerFromInstance(ctx, instance)
if err != nil {
return "", errors.Wrap(err, "fetching pool manager for instance")
}
token, err := poolMgr.GithubRunnerRegistrationToken()
if err != nil {
return "", errors.Wrap(err, "fetching runner token")
}
tokenFetched := true
updateParams := params.UpdateInstanceParams{
TokenFetched: &tokenFetched,
}
if _, err := r.store.UpdateInstance(r.ctx, instance.ID, updateParams); err != nil {
return "", errors.Wrap(err, "setting token_fetched for instance")
}
if err := r.store.AddInstanceEvent(ctx, instance.ID, params.FetchTokenEvent, params.EventInfo, "runner registration token was retrieved"); err != nil {
return "", errors.Wrap(err, "recording event")
}
return token, nil
}
func (r *Runner) getPoolManagerFromInstance(ctx context.Context, instance params.Instance) (common.PoolManager, error) {
pool, err := r.store.GetPoolByID(ctx, instance.PoolID)
if err != nil {
return nil, errors.Wrap(err, "fetching pool")
}
var poolMgr common.PoolManager
if pool.RepoID != "" {
repo, err := r.store.GetRepositoryByID(ctx, pool.RepoID)
if err != nil {
return nil, errors.Wrap(err, "fetching repo")
}
poolMgr, err = r.findRepoPoolManager(repo.Owner, repo.Name)
if err != nil {
return nil, errors.Wrapf(err, "fetching pool manager for repo %s", pool.RepoName)
}
} else if pool.OrgID != "" {
org, err := r.store.GetOrganizationByID(ctx, pool.OrgID)
if err != nil {
return nil, errors.Wrap(err, "fetching org")
}
poolMgr, err = r.findOrgPoolManager(org.Name)
if err != nil {
return nil, errors.Wrapf(err, "fetching pool manager for org %s", pool.OrgName)
}
} else if pool.EnterpriseID != "" {
enterprise, err := r.store.GetEnterpriseByID(ctx, pool.EnterpriseID)
if err != nil {
return nil, errors.Wrap(err, "fetching enterprise")
}
poolMgr, err = r.findEnterprisePoolManager(enterprise.Name)
if err != nil {
return nil, errors.Wrapf(err, "fetching pool manager for enterprise %s", pool.EnterpriseName)
}
}
return poolMgr, nil
}
func (r *Runner) ForceDeleteRunner(ctx context.Context, instanceName string) error {
if !auth.IsAdmin(ctx) {
return runnerErrors.ErrUnauthorized
@ -732,46 +824,9 @@ func (r *Runner) ForceDeleteRunner(ctx context.Context, instanceName string) err
return errors.Wrap(err, "fetching instance")
}
switch instance.Status {
case providerCommon.InstanceRunning, providerCommon.InstanceError:
default:
return runnerErrors.NewBadRequestError("runner must be in %q or %q state", providerCommon.InstanceRunning, providerCommon.InstanceError)
}
pool, err := r.store.GetPoolByID(ctx, instance.PoolID)
poolMgr, err := r.getPoolManagerFromInstance(ctx, instance)
if err != nil {
return errors.Wrap(err, "fetching pool")
}
var poolMgr common.PoolManager
if pool.RepoID != "" {
repo, err := r.store.GetRepositoryByID(ctx, pool.RepoID)
if err != nil {
return errors.Wrap(err, "fetching repo")
}
poolMgr, err = r.findRepoPoolManager(repo.Owner, repo.Name)
if err != nil {
return errors.Wrapf(err, "fetching pool manager for repo %s", pool.RepoName)
}
} else if pool.OrgID != "" {
org, err := r.store.GetOrganizationByID(ctx, pool.OrgID)
if err != nil {
return errors.Wrap(err, "fetching org")
}
poolMgr, err = r.findOrgPoolManager(org.Name)
if err != nil {
return errors.Wrapf(err, "fetching pool manager for org %s", pool.OrgName)
}
} else if pool.EnterpriseID != "" {
enterprise, err := r.store.GetEnterpriseByID(ctx, pool.EnterpriseID)
if err != nil {
return errors.Wrap(err, "fetching enterprise")
}
poolMgr, err = r.findEnterprisePoolManager(enterprise.Name)
if err != nil {
return errors.Wrapf(err, "fetching pool manager for enterprise %s", pool.EnterpriseName)
}
return errors.Wrap(err, "fetching pool manager for instance")
}
if err := poolMgr.ForceDeleteRunner(instance); err != nil {

View file

@ -4,9 +4,17 @@
# the github actions runner. Status messages can be seen by querying the
# runner status in garm.
callback_url = "https://garm.example.com/api/v1/callbacks/status"
# This URL is used by instances to retrieve information they need to set themselves
# up. Access to this URL is granted using the same JWT token used to send back
# status updates. Once the instance transitions to "installed" or "failed" state,
# access to both the status and metadata endpoints is disabled.
metadata_url = "https://garm.example.com/api/v1/metadata"
# This folder is defined here for future use. Right now, we create a SSH
# public/private key-pair.
config_dir = "/etc/garm"
# Uncomment this line if you'd like to log to a file instead of standard output.
# log_file = "/tmp/runner-manager.log"

View file

@ -24,7 +24,6 @@ import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
@ -39,6 +38,7 @@ import (
"garm/runner/common"
"github.com/google/go-github/v48/github"
gorillaHandlers "github.com/gorilla/handlers"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
@ -147,7 +147,7 @@ func GetLoggingWriter(cfg *config.Config) (io.Writer, error) {
}
func ConvertFileToBase64(file string) (string, error) {
bytes, err := ioutil.ReadFile(file)
bytes, err := os.ReadFile(file)
if err != nil {
return "", errors.Wrap(err, "reading file")
}
@ -213,7 +213,7 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne
FileName: *tools.Filename,
DownloadURL: *tools.DownloadURL,
TempDownloadToken: tempToken,
GithubToken: bootstrapParams.GithubRunnerAccessToken,
MetadataURL: bootstrapParams.MetadataURL,
RunnerUsername: config.DefaultUser,
RunnerGroup: config.DefaultUser,
RepoURL: bootstrapParams.RepoURL,
@ -321,3 +321,9 @@ func PaswsordToBcrypt(password string) (string, error) {
}
return string(hashedPassword), nil
}
func NewLoggingMiddleware(writer io.Writer) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return gorillaHandlers.CombinedLoggingHandler(writer, next)
}
}