Skip to content

Commit

Permalink
Merge pull request #154 from gabriel-samfira/add-webhook-configuration
Browse files Browse the repository at this point in the history
Add webhook management for repositories and organizations
  • Loading branch information
gabriel-samfira committed Aug 22, 2023
2 parents aa2b42f + 3b651af commit 9a7fbde
Show file tree
Hide file tree
Showing 78 changed files with 4,635 additions and 1,837 deletions.
34 changes: 27 additions & 7 deletions apiserver/controllers/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ import (
"github.com/cloudbase/garm/runner"
wsWriter "github.com/cloudbase/garm/websocket"

"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
)

func NewAPIController(r *runner.Runner, authenticator *auth.Authenticator, hub *wsWriter.Hub) (*APIController, error) {
controllerInfo, err := r.GetControllerInfo(auth.GetAdminContext())
if err != nil {
return nil, errors.Wrap(err, "failed to get controller info")
}
return &APIController{
r: r,
auth: authenticator,
Expand All @@ -43,18 +48,20 @@ func NewAPIController(r *runner.Runner, authenticator *auth.Authenticator, hub *
ReadBufferSize: 1024,
WriteBufferSize: 16384,
},
controllerID: controllerInfo.ControllerID.String(),
}, nil
}

type APIController struct {
r *runner.Runner
auth *auth.Authenticator
hub *wsWriter.Hub
upgrader websocket.Upgrader
r *runner.Runner
auth *auth.Authenticator
hub *wsWriter.Hub
upgrader websocket.Upgrader
controllerID string
}

func handleError(w http.ResponseWriter, err error) {
w.Header().Add("Content-Type", "application/json")
w.Header().Set("Content-Type", "application/json")
origErr := errors.Cause(err)
apiErr := params.APIErrorResponse{
Details: origErr.Error(),
Expand Down Expand Up @@ -138,7 +145,19 @@ func (a *APIController) handleWorkflowJobEvent(w http.ResponseWriter, r *http.Re
labelValues = a.webhookMetricLabelValues("true", "")
}

func (a *APIController) CatchAll(w http.ResponseWriter, r *http.Request) {
func (a *APIController) WebhookHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
controllerID, ok := vars["controllerID"]
// If the webhook URL includes a controller ID, we validate that it's meant for us. We still
// support bare webhook URLs, which are tipically configured manually by the user.
// The controllerID suffixed webhook URL is useful when configuring the webhook for an entity
// via garm. We cannot tag a webhook URL on github, so there is no way to determine ownership.
// Using a controllerID suffix is a simple way to denote ownership.
if ok && controllerID != a.controllerID {
log.Printf("ignoring webhook meant for controller %s", util.SanitizeLogEntry(controllerID))
return
}

headers := r.Header.Clone()

event := runnerParams.Event(headers.Get("X-Github-Event"))
Expand Down Expand Up @@ -195,8 +214,9 @@ func (a *APIController) NotFoundHandler(w http.ResponseWriter, r *http.Request)
Details: "Resource not found",
Error: "Not found",
}
w.WriteHeader(http.StatusNotFound)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
if err := json.NewEncoder(w).Encode(apiErr); err != nil {
log.Printf("failet to write response: %q", err)
}
Expand Down
164 changes: 156 additions & 8 deletions apiserver/controllers/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/json"
"log"
"net/http"
"strconv"

gErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/apiserver/params"
Expand All @@ -43,21 +44,21 @@ import (
func (a *APIController) CreateOrgHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var repoData runnerParams.CreateOrgParams
if err := json.NewDecoder(r.Body).Decode(&repoData); err != nil {
var orgData runnerParams.CreateOrgParams
if err := json.NewDecoder(r.Body).Decode(&orgData); err != nil {
handleError(w, gErrors.ErrBadRequest)
return
}

repo, err := a.r.CreateOrganization(ctx, repoData)
org, err := a.r.CreateOrganization(ctx, orgData)
if err != nil {
log.Printf("error creating repository: %+v", err)
log.Printf("error creating organization: %+v", err)
handleError(w, err)
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(repo); err != nil {
if err := json.NewEncoder(w).Encode(org); err != nil {
log.Printf("failed to encode response: %q", err)
}
}
Expand Down Expand Up @@ -139,6 +140,12 @@ func (a *APIController) GetOrgByIDHandler(w http.ResponseWriter, r *http.Request
// in: path
// required: true
//
// + name: keepWebhook
// description: If true and a webhook is installed for this organization, it will not be removed.
// type: boolean
// in: query
// required: false
//
// Responses:
// default: APIErrorResponse
func (a *APIController) DeleteOrgHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -157,7 +164,9 @@ func (a *APIController) DeleteOrgHandler(w http.ResponseWriter, r *http.Request)
return
}

if err := a.r.DeleteOrganization(ctx, orgID); err != nil {
keepWebhook, _ := strconv.ParseBool(r.URL.Query().Get("keepWebhook"))

if err := a.r.DeleteOrganization(ctx, orgID, keepWebhook); err != nil {
log.Printf("removing org: %+v", err)
handleError(w, err)
return
Expand Down Expand Up @@ -344,9 +353,9 @@ func (a *APIController) ListOrgPoolsHandler(w http.ResponseWriter, r *http.Reque
func (a *APIController) GetOrgPoolHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
orgID, repoOk := vars["orgID"]
orgID, orgOk := vars["orgID"]
poolID, poolOk := vars["poolID"]
if !repoOk || !poolOk {
if !orgOk || !poolOk {
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(params.APIErrorResponse{
Error: "Bad Request",
Expand Down Expand Up @@ -479,3 +488,142 @@ 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: HookInfo
// 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
}

info, err := a.r.InstallOrgWebhook(ctx, orgID, hookParam)
if err != nil {
log.Printf("installing webhook: %s", err)
handleError(w, err)
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(info); err != nil {
log.Printf("failed to encode response: %q", err)
}
}

// 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)
}

// swagger:route GET /organizations/{orgID}/webhook organizations hooks GetOrgWebhookInfo
//
// Get information about the GARM installed webhook on an organization.
//
// Parameters:
// + name: orgID
// description: Organization ID.
// type: string
// in: path
// required: true
//
// Responses:
// 200: HookInfo
// default: APIErrorResponse
func (a *APIController) GetOrgWebhookInfoHandler(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
}

info, err := a.r.GetOrgWebhookInfo(ctx, orgID)
if err != nil {
log.Printf("getting webhook info: %s", err)
handleError(w, err)
return
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(info); err != nil {
log.Printf("failed to encode response: %q", err)
}
}
Loading

0 comments on commit 9a7fbde

Please sign in to comment.