Add functions to (un)install webhooks for orgs and repos

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-08-15 17:19:06 +00:00
parent f2796f1d5a
commit 7ce3f007b0
No known key found for this signature in database
GPG key ID: 7D073DCC2C074CB5
27 changed files with 1366 additions and 81 deletions

View file

@ -48,7 +48,7 @@ func NewAPIController(r *runner.Runner, authenticator *auth.Authenticator, hub *
ReadBufferSize: 1024,
WriteBufferSize: 16384,
},
controllerID: controllerInfo.ShortControllerID(),
controllerID: controllerInfo.ControllerID.String(),
}, nil
}

View file

@ -479,3 +479,96 @@ func (a *APIController) UpdateOrgPoolHandler(w http.ResponseWriter, r *http.Requ
log.Printf("failed to encode response: %q", err)
}
}
// swagger:route POST /organizations/{orgID}/webhook organizations hooks InstallOrgWebhook
//
// Install the GARM webhook for an organization. The secret configured on the organization will
// be used to validate the requests.
//
// Parameters:
// + name: orgID
// description: Organization ID.
// type: string
// in: path
// required: true
//
// + name: Body
// description: Parameters used when creating the organization webhook.
// type: InstallWebhookParams
// in: body
// required: true
//
// Responses:
// 200: {}
// default: APIErrorResponse
func (a *APIController) InstallOrgWebhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
orgID, orgOk := vars["orgID"]
if !orgOk {
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No org ID specified",
}); err != nil {
log.Printf("failed to encode response: %q", err)
}
return
}
var hookParam runnerParams.InstallWebhookParams
if err := json.NewDecoder(r.Body).Decode(&hookParam); err != nil {
log.Printf("failed to decode: %s", err)
handleError(w, gErrors.ErrBadRequest)
return
}
if err := a.r.InstallOrgWebhook(ctx, orgID, hookParam); err != nil {
log.Printf("installing webhook: %s", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
// swagger:route DELETE /organizations/{orgID}/webhook organizations hooks UninstallOrgWebhook
//
// Uninstall organization webhook.
//
// Parameters:
// + name: orgID
// description: Organization ID.
// type: string
// in: path
// required: true
//
// Responses:
// default: APIErrorResponse
func (a *APIController) UninstallOrgWebhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
orgID, orgOk := vars["orgID"]
if !orgOk {
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No org ID specified",
}); err != nil {
log.Printf("failed to encode response: %q", err)
}
return
}
if err := a.r.UninstallOrgWebhook(ctx, orgID); err != nil {
log.Printf("removing webhook: %s", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}

View file

@ -479,3 +479,96 @@ func (a *APIController) UpdateRepoPoolHandler(w http.ResponseWriter, r *http.Req
log.Printf("failed to encode response: %q", err)
}
}
// swagger:route POST /repositories/{repoID}/webhook repositories hooks InstallRepoWebhook
//
// Install the GARM webhook for an organization. The secret configured on the organization will
// be used to validate the requests.
//
// Parameters:
// + name: repoID
// description: Repository ID.
// type: string
// in: path
// required: true
//
// + name: Body
// description: Parameters used when creating the repository webhook.
// type: InstallWebhookParams
// in: body
// required: true
//
// Responses:
// 200: {}
// default: APIErrorResponse
func (a *APIController) InstallRepoWebhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
repoID, orgOk := vars["repoID"]
if !orgOk {
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No repository ID specified",
}); err != nil {
log.Printf("failed to encode response: %q", err)
}
return
}
var hookParam runnerParams.InstallWebhookParams
if err := json.NewDecoder(r.Body).Decode(&hookParam); err != nil {
log.Printf("failed to decode: %s", err)
handleError(w, gErrors.ErrBadRequest)
return
}
if err := a.r.InstallRepoWebhook(ctx, repoID, hookParam); err != nil {
log.Printf("installing webhook: %s", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
// swagger:route DELETE /repositories/{repoID}/webhook repositories hooks UninstallRepoWebhook
//
// Uninstall organization webhook.
//
// Parameters:
// + name: repoID
// description: Repository ID.
// type: string
// in: path
// required: true
//
// Responses:
// default: APIErrorResponse
func (a *APIController) UninstallRepoWebhookHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
repoID, orgOk := vars["repoID"]
if !orgOk {
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Details: "No repository ID specified",
}); err != nil {
log.Printf("failed to encode response: %q", err)
}
return
}
if err := a.r.UninstallRepoWebhook(ctx, repoID); err != nil {
log.Printf("removing webhook: %s", err)
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}

View file

@ -203,6 +203,13 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
apiRouter.Handle("/repositories/", http.HandlerFunc(han.CreateRepoHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories", http.HandlerFunc(han.CreateRepoHandler)).Methods("POST", "OPTIONS")
// Install Webhook
apiRouter.Handle("/repositories/{repoID}/webhook/", http.HandlerFunc(han.InstallRepoWebhookHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/webhook", http.HandlerFunc(han.InstallRepoWebhookHandler)).Methods("POST", "OPTIONS")
// Uninstall Webhook
apiRouter.Handle("/repositories/{repoID}/webhook/", http.HandlerFunc(han.UninstallRepoWebhookHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/repositories/{repoID}/webhook", http.HandlerFunc(han.UninstallRepoWebhookHandler)).Methods("DELETE", "OPTIONS")
/////////////////////////////
// Organizations and pools //
/////////////////////////////
@ -242,6 +249,13 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
apiRouter.Handle("/organizations/", http.HandlerFunc(han.CreateOrgHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations", http.HandlerFunc(han.CreateOrgHandler)).Methods("POST", "OPTIONS")
// Install Webhook
apiRouter.Handle("/organizations/{orgID}/webhook/", http.HandlerFunc(han.InstallOrgWebhookHandler)).Methods("POST", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/webhook", http.HandlerFunc(han.InstallOrgWebhookHandler)).Methods("POST", "OPTIONS")
// Uninstall Webhook
apiRouter.Handle("/organizations/{orgID}/webhook/", http.HandlerFunc(han.UninstallOrgWebhookHandler)).Methods("DELETE", "OPTIONS")
apiRouter.Handle("/organizations/{orgID}/webhook", http.HandlerFunc(han.UninstallOrgWebhookHandler)).Methods("DELETE", "OPTIONS")
/////////////////////////////
// Enterprises and pools //
/////////////////////////////

View file

@ -15,6 +15,13 @@ definitions:
import:
package: github.com/cloudbase/garm/params
alias: garm_params
InstallWebhookParams:
type: object
x-go-type:
type: InstallWebhookParams
import:
package: github.com/cloudbase/garm/params
alias: garm_params
NewUserParams:
type: object
x-go-type:

View file

@ -76,6 +76,13 @@ definitions:
alias: garm_params
package: github.com/cloudbase/garm/params
type: GithubCredentials
InstallWebhookParams:
type: object
x-go-type:
import:
alias: garm_params
package: github.com/cloudbase/garm/params
type: InstallWebhookParams
Instance:
type: object
x-go-type:
@ -900,6 +907,53 @@ paths:
tags:
- organizations
- pools
/organizations/{orgID}/webhook:
delete:
operationId: UninstallOrgWebhook
parameters:
- description: Organization ID.
in: path
name: orgID
required: true
type: string
responses:
default:
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
summary: Uninstall organization webhook.
tags:
- organizations
- hooks
post:
description: |-
Install the GARM webhook for an organization. The secret configured on the organization will
be used to validate the requests.
operationId: InstallOrgWebhook
parameters:
- description: Organization ID.
in: path
name: orgID
required: true
type: string
- description: Parameters used when creating the organization webhook.
in: body
name: Body
required: true
schema:
$ref: '#/definitions/InstallWebhookParams'
description: Parameters used when creating the organization webhook.
type: object
responses:
"200":
$ref: '#/responses/%7B%7D'
default:
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
tags:
- organizations
- hooks
/pools:
get:
operationId: ListPools
@ -1275,6 +1329,53 @@ paths:
tags:
- repositories
- pools
/repositories/{repoID}/webhook:
delete:
operationId: UninstallRepoWebhook
parameters:
- description: Repository ID.
in: path
name: repoID
required: true
type: string
responses:
default:
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
summary: Uninstall organization webhook.
tags:
- repositories
- hooks
post:
description: |-
Install the GARM webhook for an organization. The secret configured on the organization will
be used to validate the requests.
operationId: InstallRepoWebhook
parameters:
- description: Repository ID.
in: path
name: repoID
required: true
type: string
- description: Parameters used when creating the repository webhook.
in: body
name: Body
required: true
schema:
$ref: '#/definitions/InstallWebhookParams'
description: Parameters used when creating the repository webhook.
type: object
responses:
"200":
$ref: '#/responses/%7B%7D'
default:
description: APIErrorResponse
schema:
$ref: '#/definitions/APIErrorResponse'
tags:
- repositories
- hooks
produces:
- application/json
security:

View file

@ -46,22 +46,32 @@ var infoShowCmd = &cobra.Command{
if err != nil {
return err
}
formatInfo(response.Payload)
return nil
return formatInfo(response.Payload)
},
}
func formatInfo(info params.ControllerInfo) {
func formatInfo(info params.ControllerInfo) error {
t := table.NewWriter()
header := table.Row{"Field", "Value"}
if info.WebhookURL == "" {
info.WebhookURL = "N/A"
}
if info.ControllerWebhookURL == "" {
info.ControllerWebhookURL = "N/A"
}
t.AppendHeader(header)
t.AppendRow(table.Row{"Controller ID", info.ControllerID})
t.AppendRow(table.Row{"Hostname", info.Hostname})
t.AppendRow(table.Row{"Metadata URL", info.MetadataURL})
t.AppendRow(table.Row{"Callback URL", info.CallbackURL})
t.AppendRow(table.Row{"Webhook URL Base", info.WebhookURL})
t.AppendRow(table.Row{"Controller Webhook URL", info.ControllerWebhookURL})
fmt.Println(t.Render())
return nil
}
func init() {

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
"github.com/cloudbase/garm-provider-common/util"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
"github.com/cloudbase/garm/params"
@ -25,9 +26,10 @@ import (
)
var (
enterpriseName string
enterpriseWebhookSecret string
enterpriseCreds string
enterpriseName string
enterpriseWebhookSecret string
enterpriseCreds string
enterpriseRandomWebhookSecret bool
)
// enterpriseCmd represents the enterprise command
@ -55,6 +57,14 @@ var enterpriseAddCmd = &cobra.Command{
return errNeedsInitError
}
if enterpriseRandomWebhookSecret {
secret, err := util.GetRandomString(32)
if err != nil {
return err
}
enterpriseWebhookSecret = secret
}
newEnterpriseReq := apiClientEnterprises.NewCreateEnterpriseParams()
newEnterpriseReq.Body = params.CreateEnterpriseParams{
Name: enterpriseName,
@ -179,6 +189,9 @@ func init() {
enterpriseAddCmd.Flags().StringVar(&enterpriseName, "name", "", "The name of the enterprise")
enterpriseAddCmd.Flags().StringVar(&enterpriseWebhookSecret, "webhook-secret", "", "The webhook secret for this enterprise")
enterpriseAddCmd.Flags().StringVar(&enterpriseCreds, "credentials", "", "Credentials name. See credentials list.")
enterpriseAddCmd.Flags().BoolVar(&enterpriseRandomWebhookSecret, "random-webhook-secret", false, "Generate a random webhook secret for this organization.")
enterpriseAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret")
enterpriseAddCmd.MarkFlagRequired("credentials") //nolint
enterpriseAddCmd.MarkFlagRequired("name") //nolint
enterpriseUpdateCmd.Flags().StringVar(&enterpriseWebhookSecret, "webhook-secret", "", "The webhook secret for this enterprise")

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
"github.com/cloudbase/garm-provider-common/util"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
"github.com/cloudbase/garm/params"
@ -25,9 +26,10 @@ import (
)
var (
orgName string
orgWebhookSecret string
orgCreds string
orgName string
orgWebhookSecret string
orgCreds string
orgRandomWebhookSecret bool
)
// organizationCmd represents the organization command
@ -55,6 +57,14 @@ var orgAddCmd = &cobra.Command{
return errNeedsInitError
}
if orgRandomWebhookSecret {
secret, err := util.GetRandomString(32)
if err != nil {
return err
}
orgWebhookSecret = secret
}
newOrgReq := apiClientOrgs.NewCreateOrgParams()
newOrgReq.Body = params.CreateOrgParams{
Name: orgName,
@ -179,6 +189,9 @@ func init() {
orgAddCmd.Flags().StringVar(&orgName, "name", "", "The name of the organization")
orgAddCmd.Flags().StringVar(&orgWebhookSecret, "webhook-secret", "", "The webhook secret for this organization")
orgAddCmd.Flags().StringVar(&orgCreds, "credentials", "", "Credentials name. See credentials list.")
orgAddCmd.Flags().BoolVar(&orgRandomWebhookSecret, "random-webhook-secret", false, "Generate a random webhook secret for this organization.")
orgAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret")
orgAddCmd.MarkFlagRequired("credentials") //nolint
orgAddCmd.MarkFlagRequired("name") //nolint
orgUpdateCmd.Flags().StringVar(&orgWebhookSecret, "webhook-secret", "", "The webhook secret for this organization")

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
"github.com/cloudbase/garm-provider-common/util"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
"github.com/cloudbase/garm/params"
@ -25,10 +26,11 @@ import (
)
var (
repoOwner string
repoName string
repoWebhookSecret string
repoCreds string
repoOwner string
repoName string
repoWebhookSecret string
repoCreds string
randomWebhookSecret bool
)
// repositoryCmd represents the repository command
@ -56,6 +58,14 @@ var repoAddCmd = &cobra.Command{
return errNeedsInitError
}
if randomWebhookSecret {
secret, err := util.GetRandomString(32)
if err != nil {
return err
}
repoWebhookSecret = secret
}
newRepoReq := apiClientRepos.NewCreateRepoParams()
newRepoReq.Body = params.CreateRepoParams{
Owner: repoOwner,
@ -183,10 +193,13 @@ func init() {
repoAddCmd.Flags().StringVar(&repoName, "name", "", "The name of the repository")
repoAddCmd.Flags().StringVar(&repoWebhookSecret, "webhook-secret", "", "The webhook secret for this repository")
repoAddCmd.Flags().StringVar(&repoCreds, "credentials", "", "Credentials name. See credentials list.")
repoAddCmd.Flags().BoolVar(&randomWebhookSecret, "random-webhook-secret", false, "Generate a random webhook secret for this repository.")
repoAddCmd.MarkFlagsMutuallyExclusive("webhook-secret", "random-webhook-secret")
repoAddCmd.MarkFlagRequired("credentials") //nolint
repoAddCmd.MarkFlagRequired("owner") //nolint
repoAddCmd.MarkFlagRequired("name") //nolint
repoUpdateCmd.Flags().StringVar(&repoWebhookSecret, "webhook-secret", "", "The webhook secret for this repository")
repoUpdateCmd.Flags().StringVar(&repoWebhookSecret, "webhook-secret", "", "The webhook secret for this repository. If you update this secret, you will have to manually update the secret in GitHub as well.")
repoUpdateCmd.Flags().StringVar(&repoCreds, "credentials", "", "Credentials name. See credentials list.")
repositoryCmd.AddCommand(

View file

@ -16,7 +16,6 @@ package params
import (
"encoding/json"
"math/big"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
@ -28,12 +27,13 @@ import (
)
type (
PoolType string
EventType string
EventLevel string
ProviderType string
JobStatus string
RunnerStatus string
PoolType string
EventType string
EventLevel string
ProviderType string
JobStatus string
RunnerStatus string
WebhookEndpointType string
)
const (
@ -43,6 +43,16 @@ const (
ExternalProvider ProviderType = "external"
)
const (
// WebhookEndpointDirect instructs garm that it should attempt to create a webhook
// in the target entity, using the callback URL defined in the config as a target.
WebhookEndpointDirect WebhookEndpointType = "direct"
// WebhookEndpointTunnel instructs garm that it should attempt to create a webhook
// in the target entity, using the tunnel URL as a base for the webhook URL.
// This is defined for future use.
WebhookEndpointTunnel WebhookEndpointType = "tunnel"
)
const (
JobStatusQueued JobStatus = "queued"
JobStatusInProgress JobStatus = "in_progress"
@ -286,11 +296,14 @@ func (p *Pool) PoolType() PoolType {
type Pools []Pool
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"`
OAuth2Token string `json:"oauth2"`
ControllerID string `json:"controller_id"`
InstanceCallbackURL string `json:"instance_callback_url"`
InstanceMetadataURL string `json:"instance_metadata_url"`
BaseWebhookURL string `json:"base_webhook_url"`
ControllerWebhookURL string `json:"controller_webhook_url"`
JWTSecret string `json:"jwt_secret"`
// GithubCredentialsDetails contains all info about the credentials, except the
// token, which is added above.
GithubCredentialsDetails GithubCredentials `json:"gh_creds_details"`
@ -380,20 +393,12 @@ type JWTResponse struct {
}
type ControllerInfo struct {
ControllerID uuid.UUID `json:"controller_id"`
Hostname string `json:"hostname"`
MetadataURL string `json:"metadata_url"`
CallbackURL string `json:"callback_url"`
}
func (c ControllerInfo) ShortControllerID() string {
var i big.Int
i.SetBytes(c.ControllerID[:])
id := i.Text(62)
if id == "0" {
return ""
}
return id
ControllerID uuid.UUID `json:"controller_id"`
Hostname string `json:"hostname"`
MetadataURL string `json:"metadata_url"`
CallbackURL string `json:"callback_url"`
WebhookURL string `json:"webhook_url"`
ControllerWebhookURL string `json:"controller_webhook_url"`
}
type GithubCredentials struct {
@ -495,3 +500,8 @@ type Job struct {
// used by swagger client generated code
type Jobs []Job
type InstallWebhookParams struct {
WebhookEndpointType WebhookEndpointType `json:"webhook_endpoint_type"`
InsecureSSL bool `json:"insecure_ssl"`
}

View file

@ -14,6 +14,41 @@ type GithubClient struct {
mock.Mock
}
// CreateOrgHook provides a mock function with given fields: ctx, org, hook
func (_m *GithubClient) CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, hook)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, hook)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) *github.Hook); ok {
r0 = rf(ctx, org, hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, *github.Hook) *github.Response); ok {
r1 = rf(ctx, org, hook)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, *github.Hook) error); ok {
r2 = rf(ctx, org, hook)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// CreateOrganizationRegistrationToken provides a mock function with given fields: ctx, owner
func (_m *GithubClient) CreateOrganizationRegistrationToken(ctx context.Context, owner string) (*github.RegistrationToken, *github.Response, error) {
ret := _m.Called(ctx, owner)
@ -84,6 +119,163 @@ func (_m *GithubClient) CreateRegistrationToken(ctx context.Context, owner strin
return r0, r1, r2
}
// CreateRepoHook provides a mock function with given fields: ctx, owner, repo, hook
func (_m *GithubClient) CreateRepoHook(ctx context.Context, owner string, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, hook)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, hook)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) *github.Hook); ok {
r0 = rf(ctx, owner, repo, hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.Hook) *github.Response); ok {
r1 = rf(ctx, owner, repo, hook)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.Hook) error); ok {
r2 = rf(ctx, owner, repo, hook)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// DeleteOrgHook provides a mock function with given fields: ctx, org, id
func (_m *GithubClient) DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) {
ret := _m.Called(ctx, org, id)
var r0 *github.Response
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Response, error)); ok {
return rf(ctx, org, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Response); ok {
r0 = rf(ctx, org, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Response)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok {
r1 = rf(ctx, org, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteRepoHook provides a mock function with given fields: ctx, owner, repo, id
func (_m *GithubClient) DeleteRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) {
ret := _m.Called(ctx, owner, repo, id)
var r0 *github.Response
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok {
return rf(ctx, owner, repo, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Response); ok {
r0 = rf(ctx, owner, repo, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Response)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok {
r1 = rf(ctx, owner, repo, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOrgHook provides a mock function with given fields: ctx, org, id
func (_m *GithubClient) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, id)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Hook); ok {
r0 = rf(ctx, org, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) *github.Response); ok {
r1 = rf(ctx, org, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok {
r2 = rf(ctx, org, id)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetRepoHook provides a mock function with given fields: ctx, owner, repo, id
func (_m *GithubClient) GetRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, id)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Hook); ok {
r0 = rf(ctx, owner, repo, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) *github.Response); ok {
r1 = rf(ctx, owner, repo, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, int64) error); ok {
r2 = rf(ctx, owner, repo, id)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetWorkflowJobByID provides a mock function with given fields: ctx, owner, repo, jobID
func (_m *GithubClient) GetWorkflowJobByID(ctx context.Context, owner string, repo string, jobID int64) (*github.WorkflowJob, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, jobID)
@ -119,6 +311,41 @@ func (_m *GithubClient) GetWorkflowJobByID(ctx context.Context, owner string, re
return r0, r1, r2
}
// ListOrgHooks provides a mock function with given fields: ctx, org, opts
func (_m *GithubClient) ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, opts)
var r0 []*github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) []*github.Hook); ok {
r0 = rf(ctx, org, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOptions) *github.Response); ok {
r1 = rf(ctx, org, opts)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOptions) error); ok {
r2 = rf(ctx, org, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// ListOrganizationRunnerApplicationDownloads provides a mock function with given fields: ctx, owner
func (_m *GithubClient) ListOrganizationRunnerApplicationDownloads(ctx context.Context, owner string) ([]*github.RunnerApplicationDownload, *github.Response, error) {
ret := _m.Called(ctx, owner)
@ -189,6 +416,41 @@ func (_m *GithubClient) ListOrganizationRunners(ctx context.Context, owner strin
return r0, r1, r2
}
// ListRepoHooks provides a mock function with given fields: ctx, owner, repo, opts
func (_m *GithubClient) ListRepoHooks(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, opts)
var r0 []*github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) []*github.Hook); ok {
r0 = rf(ctx, owner, repo, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.ListOptions) *github.Response); ok {
r1 = rf(ctx, owner, repo, opts)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.ListOptions) error); ok {
r2 = rf(ctx, owner, repo, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// ListRunnerApplicationDownloads provides a mock function with given fields: ctx, owner, repo
func (_m *GithubClient) ListRunnerApplicationDownloads(ctx context.Context, owner string, repo string) ([]*github.RunnerApplicationDownload, *github.Response, error) {
ret := _m.Called(ctx, owner, repo)

View file

@ -0,0 +1,160 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
context "context"
github "github.com/google/go-github/v53/github"
mock "github.com/stretchr/testify/mock"
)
// OrganizationHooks is an autogenerated mock type for the OrganizationHooks type
type OrganizationHooks struct {
mock.Mock
}
// CreateOrgHook provides a mock function with given fields: ctx, org, hook
func (_m *OrganizationHooks) CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, hook)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, hook)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *github.Hook) *github.Hook); ok {
r0 = rf(ctx, org, hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, *github.Hook) *github.Response); ok {
r1 = rf(ctx, org, hook)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, *github.Hook) error); ok {
r2 = rf(ctx, org, hook)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// DeleteOrgHook provides a mock function with given fields: ctx, org, id
func (_m *OrganizationHooks) DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) {
ret := _m.Called(ctx, org, id)
var r0 *github.Response
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Response, error)); ok {
return rf(ctx, org, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Response); ok {
r0 = rf(ctx, org, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Response)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) error); ok {
r1 = rf(ctx, org, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetOrgHook provides a mock function with given fields: ctx, org, id
func (_m *OrganizationHooks) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, id)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64) *github.Hook); ok {
r0 = rf(ctx, org, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) *github.Response); ok {
r1 = rf(ctx, org, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok {
r2 = rf(ctx, org, id)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// ListOrgHooks provides a mock function with given fields: ctx, org, opts
func (_m *OrganizationHooks) ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, org, opts)
var r0 []*github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok {
return rf(ctx, org, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOptions) []*github.Hook); ok {
r0 = rf(ctx, org, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOptions) *github.Response); ok {
r1 = rf(ctx, org, opts)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOptions) error); ok {
r2 = rf(ctx, org, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// NewOrganizationHooks creates a new instance of OrganizationHooks. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewOrganizationHooks(t interface {
mock.TestingT
Cleanup(func())
}) *OrganizationHooks {
mock := &OrganizationHooks{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -3,6 +3,8 @@
package mocks
import (
context "context"
params "github.com/cloudbase/garm/params"
mock "github.com/stretchr/testify/mock"
)
@ -78,6 +80,20 @@ func (_m *PoolManager) ID() string {
return r0
}
// InstallWebhook provides a mock function with given fields: ctx, param
func (_m *PoolManager) InstallWebhook(ctx context.Context, param params.InstallWebhookParams) error {
ret := _m.Called(ctx, param)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, params.InstallWebhookParams) error); ok {
r0 = rf(ctx, param)
} else {
r0 = ret.Error(0)
}
return r0
}
// RefreshState provides a mock function with given fields: param
func (_m *PoolManager) RefreshState(param params.UpdatePoolStateParams) error {
ret := _m.Called(param)
@ -134,6 +150,20 @@ func (_m *PoolManager) Stop() error {
return r0
}
// UninstallWebhook provides a mock function with given fields: ctx
func (_m *PoolManager) UninstallWebhook(ctx context.Context) error {
ret := _m.Called(ctx)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// Wait provides a mock function with given fields:
func (_m *PoolManager) Wait() error {
ret := _m.Called()

View file

@ -0,0 +1,160 @@
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
package mocks
import (
context "context"
github "github.com/google/go-github/v53/github"
mock "github.com/stretchr/testify/mock"
)
// RepositoryHooks is an autogenerated mock type for the RepositoryHooks type
type RepositoryHooks struct {
mock.Mock
}
// CreateRepoHook provides a mock function with given fields: ctx, owner, repo, hook
func (_m *RepositoryHooks) CreateRepoHook(ctx context.Context, owner string, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, hook)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, hook)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.Hook) *github.Hook); ok {
r0 = rf(ctx, owner, repo, hook)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.Hook) *github.Response); ok {
r1 = rf(ctx, owner, repo, hook)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.Hook) error); ok {
r2 = rf(ctx, owner, repo, hook)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// DeleteRepoHook provides a mock function with given fields: ctx, owner, repo, id
func (_m *RepositoryHooks) DeleteRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Response, error) {
ret := _m.Called(ctx, owner, repo, id)
var r0 *github.Response
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok {
return rf(ctx, owner, repo, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Response); ok {
r0 = rf(ctx, owner, repo, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Response)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok {
r1 = rf(ctx, owner, repo, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRepoHook provides a mock function with given fields: ctx, owner, repo, id
func (_m *RepositoryHooks) GetRepoHook(ctx context.Context, owner string, repo string, id int64) (*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, id)
var r0 *github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, id)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) *github.Hook); ok {
r0 = rf(ctx, owner, repo, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) *github.Response); ok {
r1 = rf(ctx, owner, repo, id)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, int64) error); ok {
r2 = rf(ctx, owner, repo, id)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// ListRepoHooks provides a mock function with given fields: ctx, owner, repo, opts
func (_m *RepositoryHooks) ListRepoHooks(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
ret := _m.Called(ctx, owner, repo, opts)
var r0 []*github.Hook
var r1 *github.Response
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) ([]*github.Hook, *github.Response, error)); ok {
return rf(ctx, owner, repo, opts)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.ListOptions) []*github.Hook); ok {
r0 = rf(ctx, owner, repo, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*github.Hook)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.ListOptions) *github.Response); ok {
r1 = rf(ctx, owner, repo, opts)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.ListOptions) error); ok {
r2 = rf(ctx, owner, repo, opts)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// NewRepositoryHooks creates a new instance of RepositoryHooks. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewRepositoryHooks(t interface {
mock.TestingT
Cleanup(func())
}) *RepositoryHooks {
mock := &RepositoryHooks{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -15,6 +15,7 @@
package common
import (
"context"
"time"
"github.com/cloudbase/garm/params"
@ -43,7 +44,8 @@ type PoolManager interface {
HandleWorkflowJob(job params.WorkflowJob) error
RefreshState(param params.UpdatePoolStateParams) error
ForceDeleteRunner(runner params.Instance) error
// AddPool(ctx context.Context, pool params.Pool) error
InstallWebhook(ctx context.Context, param params.InstallWebhookParams) error
UninstallWebhook(ctx context.Context) error
// PoolManager lifecycle functions. Start/stop pool.
Start() error

View file

@ -6,11 +6,28 @@ import (
"github.com/google/go-github/v53/github"
)
type OrganizationHooks interface {
ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error)
GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error)
CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error)
DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error)
}
type RepositoryHooks interface {
ListRepoHooks(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error)
GetRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Hook, *github.Response, error)
CreateRepoHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, *github.Response, error)
DeleteRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error)
}
// GithubClient that describes the minimum list of functions we need to interact with github.
// Allows for easier testing.
//
//go:generate mockery --all
type GithubClient interface {
OrganizationHooks
RepositoryHooks
// GetWorkflowJobByID gets details about a single workflow job.
GetWorkflowJobByID(ctx context.Context, owner, repo string, jobID int64) (*github.WorkflowJob, *github.Response, error)
// ListRunners lists all runners within a repository.

View file

@ -339,3 +339,45 @@ func (r *Runner) findOrgPoolManager(name string) (common.PoolManager, error) {
}
return poolManager, nil
}
func (r *Runner) InstallOrgWebhook(ctx context.Context, orgID string, param params.InstallWebhookParams) error {
if !auth.IsAdmin(ctx) {
return runnerErrors.ErrUnauthorized
}
org, err := r.store.GetOrganizationByID(ctx, orgID)
if err != nil {
return errors.Wrap(err, "fetching org")
}
poolMgr, err := r.poolManagerCtrl.GetOrgPoolManager(org)
if err != nil {
return errors.Wrap(err, "fetching pool manager for org")
}
if err := poolMgr.InstallWebhook(ctx, param); err != nil {
return errors.Wrap(err, "installing webhook")
}
return nil
}
func (r *Runner) UninstallOrgWebhook(ctx context.Context, orgID string) error {
if !auth.IsAdmin(ctx) {
return runnerErrors.ErrUnauthorized
}
org, err := r.store.GetOrganizationByID(ctx, orgID)
if err != nil {
return errors.Wrap(err, "fetching org")
}
poolMgr, err := r.poolManagerCtrl.GetOrgPoolManager(org)
if err != nil {
return errors.Wrap(err, "fetching pool manager for org")
}
if err := poolMgr.UninstallWebhook(ctx); err != nil {
return errors.Wrap(err, "uninstalling webhook")
}
return nil
}

View file

@ -197,14 +197,6 @@ func (r *enterprise) WebhookSecret() string {
return r.cfg.WebhookSecret
}
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 {
@ -231,3 +223,11 @@ func (r *enterprise) ValidateOwner(job params.WorkflowJob) error {
func (r *enterprise) ID() string {
return r.id
}
func (r *enterprise) InstallHook(ctx context.Context, req *github.Hook) error {
return fmt.Errorf("not implemented")
}
func (r *enterprise) UninstallHook(ctx context.Context, url string) error {
return fmt.Errorf("not implemented")
}

View file

@ -15,6 +15,8 @@
package pool
import (
"context"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner/common"
@ -29,6 +31,9 @@ type poolHelper interface {
RemoveGithubRunner(runnerID int64) (*github.Response, error)
FetchTools() ([]*github.RunnerApplicationDownload, error)
InstallHook(ctx context.Context, req *github.Hook) error
UninstallHook(ctx context.Context, url string) error
GithubCLI() common.GithubClient
FetchDbInstances() ([]params.Instance, error)
@ -36,8 +41,6 @@ type poolHelper interface {
GithubURL() string
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

@ -57,6 +57,12 @@ func NewOrganizationPoolManager(ctx context.Context, cfg params.Organization, cf
store: store,
providers: providers,
controllerID: cfgInternal.ControllerID,
urls: urls{
webhookURL: cfgInternal.BaseWebhookURL,
callbackURL: cfgInternal.InstanceCallbackURL,
metadataURL: cfgInternal.InstanceMetadataURL,
controllerWebhookURL: cfgInternal.ControllerWebhookURL,
},
quit: make(chan struct{}),
helper: helper,
credsDetails: cfgInternal.GithubCredentialsDetails,
@ -210,14 +216,6 @@ func (r *organization) WebhookSecret() string {
return r.cfg.WebhookSecret
}
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 {
@ -244,3 +242,59 @@ func (r *organization) ValidateOwner(job params.WorkflowJob) error {
func (r *organization) ID() string {
return r.id
}
func (r *organization) listHooks(ctx context.Context) ([]*github.Hook, error) {
opts := github.ListOptions{
PerPage: 100,
}
var allHooks []*github.Hook
for {
hooks, ghResp, err := r.ghcli.ListOrgHooks(ctx, r.cfg.Name, &opts)
if err != nil {
return nil, errors.Wrap(err, "fetching hooks")
}
allHooks = append(allHooks, hooks...)
if ghResp.NextPage == 0 {
break
}
opts.Page = ghResp.NextPage
}
return allHooks, nil
}
func (r *organization) InstallHook(ctx context.Context, req *github.Hook) error {
allHooks, err := r.listHooks(ctx)
if err != nil {
return errors.Wrap(err, "listing hooks")
}
for _, hook := range allHooks {
if hook.Config["url"] == req.Config["url"] {
return fmt.Errorf("hook already installed: %w", runnerErrors.ErrBadRequest)
}
}
_, _, err = r.ghcli.CreateOrgHook(ctx, r.cfg.Name, req)
if err != nil {
return errors.Wrap(err, "creating organization hook")
}
return nil
}
func (r *organization) UninstallHook(ctx context.Context, url string) error {
allHooks, err := r.listHooks(ctx)
if err != nil {
return errors.Wrap(err, "listing hooks")
}
for _, hook := range allHooks {
if hook.Config["url"] == url {
_, err = r.ghcli.DeleteOrgHook(ctx, r.cfg.Name, hook.GetID())
if err != nil {
return errors.Wrap(err, "deleting hook")
}
return nil
}
}
return nil
}

View file

@ -85,6 +85,12 @@ func (k *keyMutex) Delete(key string) {
k.muxes.Delete(key)
}
type urls struct {
callbackURL string
metadataURL string
webhookURL string
controllerWebhookURL string
}
type basePoolManager struct {
ctx context.Context
controllerID string
@ -101,6 +107,8 @@ type basePoolManager struct {
managerIsRunning bool
managerErrorReason string
urls urls
mux sync.Mutex
wg *sync.WaitGroup
keyMux *keyMutex
@ -686,8 +694,8 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditiona
RunnerStatus: params.RunnerPending,
OSArch: pool.OSArch,
OSType: pool.OSType,
CallbackURL: r.helper.GetCallbackURL(),
MetadataURL: r.helper.GetMetadataURL(),
CallbackURL: r.urls.callbackURL,
MetadataURL: r.urls.metadataURL,
CreateAttempt: 1,
GitHubRunnerGroup: pool.GitHubRunnerGroup,
AditionalLabels: aditionalLabels,
@ -1598,3 +1606,36 @@ func (r *basePoolManager) consumeQueuedJobs() error {
}
return nil
}
func (r *basePoolManager) InstallWebhook(ctx context.Context, param params.InstallWebhookParams) error {
if r.urls.controllerWebhookURL == "" {
return errors.Wrap(runnerErrors.ErrUnprocessable, "controller webhook url is empty")
}
insecureSSL := "0"
if param.InsecureSSL {
insecureSSL = "1"
}
req := &github.Hook{
Active: github.Bool(true),
Config: map[string]interface{}{
"url": r.urls.controllerWebhookURL,
"content_type": "json",
"insecure_ssl": insecureSSL,
"secret": r.WebhookSecret(),
},
Events: []string{
"workflow_job",
},
}
return r.helper.InstallHook(ctx, req)
}
func (r *basePoolManager) UninstallWebhook(ctx context.Context) error {
if r.urls.controllerWebhookURL == "" {
return errors.Wrap(runnerErrors.ErrUnprocessable, "controller webhook url is empty")
}
return r.helper.UninstallHook(ctx, r.urls.controllerWebhookURL)
}

View file

@ -211,14 +211,6 @@ func (r *repository) WebhookSecret() string {
return r.cfg.WebhookSecret
}
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 {
@ -245,3 +237,59 @@ func (r *repository) ValidateOwner(job params.WorkflowJob) error {
func (r *repository) ID() string {
return r.id
}
func (r *repository) listHooks(ctx context.Context) ([]*github.Hook, error) {
opts := github.ListOptions{
PerPage: 100,
}
var allHooks []*github.Hook
for {
hooks, ghResp, err := r.ghcli.ListRepoHooks(ctx, r.cfg.Owner, r.cfg.Name, &opts)
if err != nil {
return nil, errors.Wrap(err, "fetching hooks")
}
allHooks = append(allHooks, hooks...)
if ghResp.NextPage == 0 {
break
}
opts.Page = ghResp.NextPage
}
return allHooks, nil
}
func (r *repository) InstallHook(ctx context.Context, req *github.Hook) error {
allHooks, err := r.listHooks(ctx)
if err != nil {
return errors.Wrap(err, "listing hooks")
}
for _, hook := range allHooks {
if hook.Config["url"] == req.Config["url"] {
return fmt.Errorf("hook already installed: %w", runnerErrors.ErrBadRequest)
}
}
_, _, err = r.ghcli.CreateRepoHook(ctx, r.cfg.Owner, r.cfg.Name, req)
if err != nil {
return errors.Wrap(err, "creating repository hook")
}
return nil
}
func (r *repository) UninstallHook(ctx context.Context, url string) error {
allHooks, err := r.listHooks(ctx)
if err != nil {
return errors.Wrap(err, "listing hooks")
}
for _, hook := range allHooks {
if hook.Config["url"] == url {
_, err = r.ghcli.DeleteRepoHook(ctx, r.cfg.Owner, r.cfg.Name, *hook.ID)
if err != nil {
return errors.Wrap(err, "deleting hook")
}
return nil
}
}
return nil
}

View file

@ -349,3 +349,45 @@ func (r *Runner) findRepoPoolManager(owner, name string) (common.PoolManager, er
}
return poolManager, nil
}
func (r *Runner) InstallRepoWebhook(ctx context.Context, repoID string, param params.InstallWebhookParams) error {
if !auth.IsAdmin(ctx) {
return runnerErrors.ErrUnauthorized
}
repo, err := r.store.GetRepositoryByID(ctx, repoID)
if err != nil {
return errors.Wrap(err, "fetching repo")
}
poolManager, err := r.poolManagerCtrl.GetRepoPoolManager(repo)
if err != nil {
return errors.Wrap(err, "fetching pool manager for repo")
}
if err := poolManager.InstallWebhook(ctx, param); err != nil {
return errors.Wrap(err, "installing webhook")
}
return nil
}
func (r *Runner) UninstallRepoWebhook(ctx context.Context, repoID string) error {
if !auth.IsAdmin(ctx) {
return runnerErrors.ErrUnauthorized
}
repo, err := r.store.GetRepositoryByID(ctx, repoID)
if err != nil {
return errors.Wrap(err, "fetching repo")
}
poolManager, err := r.poolManagerCtrl.GetRepoPoolManager(repo)
if err != nil {
return errors.Wrap(err, "fetching pool manager for repo")
}
if err := poolManager.UninstallWebhook(ctx); err != nil {
return errors.Wrap(err, "uninstalling webhook")
}
return nil
}

View file

@ -311,12 +311,18 @@ func (p *poolManagerCtrl) getInternalConfig(credsName string) (params.Internal,
return params.Internal{}, fmt.Errorf("fetching CA bundle for creds: %w", err)
}
var controllerWebhookURL string
if p.config.Default.WebhookURL != "" {
controllerWebhookURL = fmt.Sprintf("%s/%s", p.config.Default.WebhookURL, p.controllerID)
}
return params.Internal{
OAuth2Token: creds.OAuth2Token,
ControllerID: p.controllerID,
InstanceCallbackURL: p.config.Default.CallbackURL,
InstanceMetadataURL: p.config.Default.MetadataURL,
JWTSecret: p.config.JWTAuth.Secret,
OAuth2Token: creds.OAuth2Token,
ControllerID: p.controllerID,
InstanceCallbackURL: p.config.Default.CallbackURL,
InstanceMetadataURL: p.config.Default.MetadataURL,
BaseWebhookURL: p.config.Default.WebhookURL,
ControllerWebhookURL: controllerWebhookURL,
JWTSecret: p.config.JWTAuth.Secret,
GithubCredentialsDetails: params.GithubCredentials{
Name: creds.Name,
Description: creds.Description,
@ -376,11 +382,17 @@ func (r *Runner) GetControllerInfo(ctx context.Context) (params.ControllerInfo,
return params.ControllerInfo{}, errors.Wrap(err, "fetching hostname")
}
r.controllerInfo.Hostname = hostname
var controllerWebhook string
if r.controllerID != uuid.Nil && r.config.Default.WebhookURL != "" {
controllerWebhook = fmt.Sprintf("%s/%s", r.config.Default.WebhookURL, r.controllerID.String())
}
return params.ControllerInfo{
ControllerID: r.controllerID,
Hostname: hostname,
MetadataURL: r.config.Default.MetadataURL,
CallbackURL: r.config.Default.CallbackURL,
ControllerID: r.controllerID,
Hostname: hostname,
MetadataURL: r.config.Default.MetadataURL,
CallbackURL: r.config.Default.CallbackURL,
WebhookURL: r.config.Default.WebhookURL,
ControllerWebhookURL: controllerWebhook,
}, nil
}

View file

@ -17,6 +17,8 @@ callback_url = "https://garm.example.com/api/v1/callbacks"
# highly encouraged.
metadata_url = "https://garm.example.com/api/v1/metadata"
webhook_url = "https://garm.example.com/webhooks"
# Uncomment this line if you'd like to log to a file instead of standard output.
# log_file = "/tmp/runner-manager.log"

View file

@ -29,6 +29,44 @@ import (
"golang.org/x/oauth2"
)
type githubClient struct {
*github.ActionsService
org *github.OrganizationsService
repo *github.RepositoriesService
}
func (g *githubClient) ListOrgHooks(ctx context.Context, org string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
return g.org.ListHooks(ctx, org, opts)
}
func (g *githubClient) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) {
return g.org.GetHook(ctx, org, id)
}
func (g *githubClient) CreateOrgHook(ctx context.Context, org string, hook *github.Hook) (*github.Hook, *github.Response, error) {
return g.org.CreateHook(ctx, org, hook)
}
func (g *githubClient) DeleteOrgHook(ctx context.Context, org string, id int64) (*github.Response, error) {
return g.org.DeleteHook(ctx, org, id)
}
func (g *githubClient) ListRepoHooks(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
return g.repo.ListHooks(ctx, owner, repo, opts)
}
func (g *githubClient) GetRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Hook, *github.Response, error) {
return g.repo.GetHook(ctx, owner, repo, id)
}
func (g *githubClient) CreateRepoHook(ctx context.Context, owner, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) {
return g.repo.CreateHook(ctx, owner, repo, hook)
}
func (g *githubClient) DeleteRepoHook(ctx context.Context, owner, repo string, id int64) (*github.Response, error) {
return g.repo.DeleteHook(ctx, owner, repo, id)
}
func GithubClient(ctx context.Context, token string, credsDetails params.GithubCredentials) (common.GithubClient, common.GithubEnterpriseClient, error) {
var roots *x509.CertPool
if credsDetails.CABundle != nil && len(credsDetails.CABundle) > 0 {
@ -56,5 +94,10 @@ func GithubClient(ctx context.Context, token string, credsDetails params.GithubC
return nil, nil, errors.Wrap(err, "fetching github client")
}
return ghClient.Actions, ghClient.Enterprise, nil
cli := &githubClient{
ActionsService: ghClient.Actions,
org: ghClient.Organizations,
repo: ghClient.Repositories,
}
return cli, ghClient.Enterprise, nil
}