Skip to content

Commit 7af3562

Browse files
committed
Added webhook handler for reload APIs
1 parent ffee448 commit 7af3562

File tree

2 files changed

+123
-4
lines changed

2 files changed

+123
-4
lines changed

internal/server/router.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package server
55

66
import (
7+
"crypto/subtle"
78
"encoding/json"
89
"fmt"
910
"net/http"
@@ -129,6 +130,8 @@ func NewTCPHandler(logger *types.Logger, config *types.ServerConfig, server *Ser
129130
router.Mount(types.INTERNAL_URL_PREFIX, http.NotFoundHandler()) // reserve the path
130131
}
131132

133+
router.Mount(types.WEBHOOK_URL_PREFIX, handler.serveWebhooks())
134+
132135
server.ssoAuth.RegisterRoutes(router) // register SSO routes
133136

134137
router.HandleFunc("/*", handler.callApp)
@@ -210,6 +213,87 @@ func (h *Handler) apiHandler(w http.ResponseWriter, r *http.Request, enableBasic
210213
}
211214
}
212215

216+
func (h *Handler) webhookHandler(w http.ResponseWriter, r *http.Request, webhookType types.WebhookType) {
217+
appPath := r.URL.Query().Get("appPath")
218+
if appPath == "" {
219+
http.Error(w, "appPath is required for webhook call", http.StatusBadRequest)
220+
return
221+
}
222+
appPathDomain, err := parseAppPath(appPath)
223+
if err != nil {
224+
http.Error(w, err.Error(), http.StatusBadRequest)
225+
return
226+
}
227+
228+
app, err := h.server.GetApp(appPathDomain, false)
229+
if err != nil {
230+
http.Error(w, err.Error(), http.StatusBadRequest)
231+
}
232+
233+
authHeader := r.Header.Get("Authorization")
234+
if !strings.HasPrefix(authHeader, "Bearer ") {
235+
http.Error(w, "Authorization header with bearer token is required", http.StatusUnauthorized)
236+
return
237+
}
238+
authToken := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
239+
if authToken == "" {
240+
http.Error(w, "Bearer token is required", http.StatusUnauthorized)
241+
return
242+
}
243+
244+
appToken := ""
245+
promote := false
246+
switch webhookType {
247+
case types.WebhookReload:
248+
appToken = app.Settings.WebhookTokens.Reload
249+
case types.WebhookReloadPromote:
250+
appToken = app.Settings.WebhookTokens.ReloadPromote
251+
promote = true
252+
default:
253+
http.Error(w, "Invalid webhook type", http.StatusInternalServerError)
254+
return
255+
}
256+
257+
if appToken == "" {
258+
http.Error(w, "Webhook is not enabled for app", http.StatusBadRequest)
259+
return
260+
}
261+
262+
if subtle.ConstantTimeCompare([]byte(appToken), []byte(authToken)) != 1 {
263+
http.Error(w, "Invalid bearer token", http.StatusUnauthorized)
264+
return
265+
}
266+
267+
payload := map[string]any{}
268+
err = json.NewDecoder(r.Body).Decode(&payload)
269+
if err != nil {
270+
http.Error(w, "Error parsing request, expected JSON", http.StatusBadRequest)
271+
return
272+
}
273+
274+
h.Trace().Str("method", r.Method).Str("url", r.URL.String()).Msg("API Received request")
275+
resp, err := h.server.ReloadApps(r.Context(), appPath, false, promote, false, "", "", "")
276+
if err != nil {
277+
if reqError, ok := err.(types.RequestError); ok {
278+
w.Header().Add("Content-Type", "application/json")
279+
errStr, _ := json.Marshal(reqError)
280+
http.Error(w, string(errStr), reqError.Code)
281+
return
282+
}
283+
h.Error().Err(err).Msg("error in api func call")
284+
http.Error(w, err.Error(), http.StatusInternalServerError)
285+
return
286+
}
287+
288+
w.Header().Add("Content-Type", "application/json")
289+
err = json.NewEncoder(w).Encode(resp)
290+
if err != nil {
291+
h.Error().Err(err).Msg("error encoding response")
292+
http.Error(w, err.Error(), http.StatusInternalServerError)
293+
return
294+
}
295+
}
296+
213297
func parseBoolArg(arg string, defaultValue bool) (bool, error) {
214298
if arg != "" {
215299
ret, err := strconv.ParseBool(arg)
@@ -551,6 +635,7 @@ func (h *Handler) versionSwitch(r *http.Request) (any, error) {
551635
return ret, nil
552636
}
553637

638+
// serveInternal returns a handler for the internal APIs for app admin and management
554639
func (h *Handler) serveInternal(enableBasicAuth bool) http.Handler {
555640
// These API's are mounted at /_clace
556641
r := chi.NewRouter()
@@ -632,3 +717,23 @@ func (h *Handler) serveInternal(enableBasicAuth bool) http.Handler {
632717

633718
return r
634719
}
720+
721+
// serveWebhooks returns a handler for the app webhooks for reload and other events
722+
// webhooks are always mounted, even if admin over TCP is not enabled. At the app
723+
// level, webhooks are disabled by default and need to be enabled by the user
724+
func (h *Handler) serveWebhooks() http.Handler {
725+
// These API's are mounted at /_clace_webhook
726+
r := chi.NewRouter()
727+
728+
// Reload app
729+
r.Post("/reload", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
730+
h.webhookHandler(w, r, types.WebhookReload)
731+
}))
732+
733+
// Reload and Promote app
734+
r.Post("/reload_promote", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
735+
h.webhookHandler(w, r, types.WebhookReloadPromote)
736+
}))
737+
738+
return r
739+
}

internal/types/types.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
ID_PREFIX_APP_STAGE = "app_stg_"
1919
ID_PREFIX_APP_PREVIEW = "app_pre_"
2020
INTERNAL_URL_PREFIX = "/_clace"
21+
WEBHOOK_URL_PREFIX = "/_clace_webhook"
2122
APP_INTERNAL_URL_PREFIX = "/_clace_app"
2223
INTERNAL_APP_DELIM = "_cl_"
2324
STAGE_SUFFIX = INTERNAL_APP_DELIM + "stage"
@@ -269,12 +270,25 @@ type AppMetadata struct {
269270

270271
// AppSettings contains the settings for an app. Settings are not version controlled.
271272
type AppSettings struct {
272-
AuthnType AppAuthnType `json:"authn_type"`
273-
GitAuthName string `json:"git_auth_name"`
274-
StageWriteAccess bool `json:"stage_write_access"`
275-
PreviewWriteAccess bool `json:"preview_write_access"`
273+
AuthnType AppAuthnType `json:"authn_type"`
274+
GitAuthName string `json:"git_auth_name"`
275+
StageWriteAccess bool `json:"stage_write_access"`
276+
PreviewWriteAccess bool `json:"preview_write_access"`
277+
WebhookTokens WebhookTokens `json:"webhook_tokens"`
276278
}
277279

280+
type WebhookTokens struct {
281+
Reload string `json:"reload"`
282+
ReloadPromote string `json:"reload_promote"`
283+
}
284+
285+
type WebhookType string
286+
287+
const (
288+
WebhookReload WebhookType = "reload"
289+
WebhookReloadPromote WebhookType = "reload_promote"
290+
)
291+
278292
// SpecFiles is a map of file names to file data. JSON encoding uses base 64 encoding of file text
279293
type SpecFiles map[string]string
280294

0 commit comments

Comments
 (0)