From 59d7233055b781ef7c85239243d83ea2c926952f Mon Sep 17 00:00:00 2001 From: Salah Aldeen Al Saleh Date: Fri, 6 Oct 2023 23:02:30 +0000 Subject: [PATCH] Add an endpoint to KOTS where application metrics can be retrieved adhoc --- pkg/apiserver/server.go | 6 +++ pkg/handlers/handlers.go | 8 +++- pkg/handlers/interface.go | 3 ++ .../{custom_metrics.go => metrics.go} | 20 ++++---- ...custom_metrics_test.go => metrics_test.go} | 14 +++--- pkg/handlers/middleware.go | 18 +++++++ pkg/handlers/mock/mock.go | 12 +++++ pkg/handlers/session.go | 47 +++++++++++++++++++ pkg/policy/middleware.go | 15 ------ pkg/replicatedapp/api.go | 2 +- 10 files changed, 113 insertions(+), 32 deletions(-) rename pkg/handlers/{custom_metrics.go => metrics.go} (77%) rename pkg/handlers/{custom_metrics_test.go => metrics_test.go} (88%) diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 8938f0986f..acea21be6c 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -187,6 +187,12 @@ func Start(params *APIServerParams) { handlers.RegisterTokenAuthRoutes(handler, debugRouter, loggingRouter) + /********************************************************************** + * License ID auth routes + **********************************************************************/ + + handlers.RegisterLicenseIDAuthRoutes(r.PathPrefix("").Subrouter(), kotsStore, handler) + /********************************************************************** * Session auth routes **********************************************************************/ diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 11b2c708c2..a7bca329e7 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -341,7 +341,13 @@ func RegisterUnauthenticatedRoutes(handler *Handler, kotsStore store.Store, debu // These handlers should be called by the application only. loggingRouter.Path("/license/v1/license").Methods("GET").HandlerFunc(handler.GetPlatformLicenseCompatibility) - loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomApplicationMetricsHandler(kotsStore)) + loggingRouter.Path("/api/v1/app/custom-metrics").Methods("POST").HandlerFunc(handler.GetSendCustomAppMetricsHandler(kotsStore)) +} + +func RegisterLicenseIDAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOTSHandler) { + r.Use(LoggingMiddleware, RequireValidLicenseMiddleware(kotsStore)) + + r.Name("GetAppMetrics").Path("/api/v1/app/metrics").Methods("GET").HandlerFunc(handler.GetAppMetrics) } func StreamJSON(c *websocket.Conn, payload interface{}) { diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index 40d7f22bdf..b088d32e07 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -155,4 +155,7 @@ type KOTSHandler interface { // Helm IsHelmManaged(w http.ResponseWriter, r *http.Request) GetAppValuesFile(w http.ResponseWriter, r *http.Request) + + // APIs available to applications (except legacy /license/v1/license) + GetAppMetrics(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/handlers/custom_metrics.go b/pkg/handlers/metrics.go similarity index 77% rename from pkg/handlers/custom_metrics.go rename to pkg/handlers/metrics.go index 4f658ae7a2..44ea1d4e33 100644 --- a/pkg/handlers/custom_metrics.go +++ b/pkg/handlers/metrics.go @@ -13,13 +13,17 @@ import ( "github.com/replicatedhq/kots/pkg/store" ) -type SendCustomApplicationMetricsRequest struct { - Data ApplicationMetricsData `json:"data"` +func (h *Handler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + JSON(w, http.StatusOK, "") } -type ApplicationMetricsData map[string]interface{} +type SendCustomAppMetricsRequest struct { + Data CustomAppMetricsData `json:"data"` +} + +type CustomAppMetricsData map[string]interface{} -func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) http.HandlerFunc { +func (h *Handler) GetSendCustomAppMetricsHandler(kotsStore store.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if kotsadm.IsAirgap() { w.WriteHeader(http.StatusForbidden) @@ -48,20 +52,20 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) return } - request := SendCustomApplicationMetricsRequest{} + request := SendCustomAppMetricsRequest{} if err := json.NewDecoder(r.Body).Decode(&request); err != nil { logger.Error(errors.Wrap(err, "decode request")) w.WriteHeader(http.StatusBadRequest) return } - if err := validateCustomMetricsData(request.Data); err != nil { + if err := validateCustomAppMetricsData(request.Data); err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } - err = replicatedapp.SendApplicationMetricsData(license, app, request.Data) + err = replicatedapp.SendCustomAppMetricsData(license, app, request.Data) if err != nil { logger.Error(errors.Wrap(err, "set application data")) w.WriteHeader(http.StatusBadRequest) @@ -72,7 +76,7 @@ func (h *Handler) GetSendCustomApplicationMetricsHandler(kotsStore store.Store) } } -func validateCustomMetricsData(data ApplicationMetricsData) error { +func validateCustomAppMetricsData(data CustomAppMetricsData) error { if len(data) == 0 { return errors.New("no data provided") } diff --git a/pkg/handlers/custom_metrics_test.go b/pkg/handlers/metrics_test.go similarity index 88% rename from pkg/handlers/custom_metrics_test.go rename to pkg/handlers/metrics_test.go index 8e0b3bda1b..56766d0cac 100644 --- a/pkg/handlers/custom_metrics_test.go +++ b/pkg/handlers/metrics_test.go @@ -19,12 +19,12 @@ import ( func Test_validateCustomMetricsData(t *testing.T) { tests := []struct { name string - data ApplicationMetricsData + data CustomAppMetricsData wantErr bool }{ { name: "all values are valid", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": "val1", "key2": 6, "key3": 6.6, @@ -34,12 +34,12 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "no data", - data: ApplicationMetricsData{}, + data: CustomAppMetricsData{}, wantErr: true, }, { name: "array value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": []string{"val1", "val2"}, }, @@ -47,7 +47,7 @@ func Test_validateCustomMetricsData(t *testing.T) { }, { name: "map value", - data: ApplicationMetricsData{ + data: CustomAppMetricsData{ "key1": 10, "key2": map[string]string{"key1": "val1"}, }, @@ -67,7 +67,7 @@ func Test_validateCustomMetricsData(t *testing.T) { } } -func Test_SendCustomApplicationMetrics(t *testing.T) { +func Test_SendCustomAppMetrics(t *testing.T) { req := require.New(t) customMetricsData := []byte(`{"data":{"key1_string":"val1","key2_int":5,"key3_float":1.5,"key4_numeric_string":"1.6"}}`) appID := "app-id-123" @@ -114,7 +114,7 @@ spec: // Validate - handler.GetSendCustomApplicationMetricsHandler(mockStore)(clientWriter, clientRequest) + handler.GetSendCustomAppMetricsHandler(mockStore)(clientWriter, clientRequest) req.Equal(http.StatusOK, clientWriter.Code) } diff --git a/pkg/handlers/middleware.go b/pkg/handlers/middleware.go index ea328f8a89..82ca484693 100644 --- a/pkg/handlers/middleware.go +++ b/pkg/handlers/middleware.go @@ -107,3 +107,21 @@ func RequireValidSessionQuietMiddleware(kotsStore store.Store) mux.MiddlewareFun }) } } + +func RequireValidLicenseMiddleware(kotsStore store.Store) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + license, app, err := requireValidLicense(kotsStore, w, r) + if err != nil { + if !kotsStore.IsNotFound(err) { + logger.Error(errors.Wrapf(err, "request %q", r.RequestURI)) + } + return + } + + r = session.ContextSetLicense(r, license) + r = session.ContextSetApp(r, app) + next.ServeHTTP(w, r) + }) + } +} diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index 45f4d1c242..1eb9ad7603 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -550,6 +550,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetAppIdentityServiceConfig(w, r interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppIdentityServiceConfig", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppIdentityServiceConfig), w, r) } +// GetAppMetrics mocks base method. +func (m *MockKOTSHandler) GetAppMetrics(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetAppMetrics", w, r) +} + +// GetAppMetrics indicates an expected call of GetAppMetrics. +func (mr *MockKOTSHandlerMockRecorder) GetAppMetrics(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppMetrics", reflect.TypeOf((*MockKOTSHandler)(nil).GetAppMetrics), w, r) +} + // GetAppRegistry mocks base method. func (m *MockKOTSHandler) GetAppRegistry(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() diff --git a/pkg/handlers/session.go b/pkg/handlers/session.go index 2179f996ed..819e41db1c 100644 --- a/pkg/handlers/session.go +++ b/pkg/handlers/session.go @@ -8,13 +8,16 @@ import ( "time" "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/handlers/types" "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" "github.com/replicatedhq/kots/pkg/session" sessiontypes "github.com/replicatedhq/kots/pkg/session/types" "github.com/replicatedhq/kots/pkg/store" "github.com/replicatedhq/kots/pkg/util" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -155,3 +158,47 @@ func requireValidKOTSToken(w http.ResponseWriter, r *http.Request) error { return errors.New("invalid auth") } + +func requireValidLicense(kotsStore store.Store, w http.ResponseWriter, r *http.Request) (*kotsv1beta1.License, *apptypes.App, error) { + if r.Method == "OPTIONS" { + return nil, nil, nil + } + + licenseID := r.Header.Get("authorization") + if licenseID == "" { + err := errors.New("missing authorization header") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + apps, err := kotsStore.ListInstalledApps() + if err != nil { + return nil, nil, errors.Wrap(err, "get all apps") + } + + var license *kotsv1beta1.License + var app *apptypes.App + + for _, a := range apps { + l, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return nil, nil, errors.Wrap(err, "load license") + } + + if l.Spec.LicenseID == licenseID { + license = l + app = a + break + } + } + + if license == nil { + err := errors.New("license ID is not valid") + response := types.ErrorResponse{Error: util.StrPointer(err.Error())} + JSON(w, http.StatusUnauthorized, response) + return nil, nil, err + } + + return license, app, nil +} diff --git a/pkg/policy/middleware.go b/pkg/policy/middleware.go index 452bf2925f..3e3504880d 100644 --- a/pkg/policy/middleware.go +++ b/pkg/policy/middleware.go @@ -75,21 +75,6 @@ func (m *Middleware) EnforceAccess(p *Policy, handler http.HandlerFunc) http.Han } } -func (m *Middleware) EnforceLicense(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - license := session.ContextGetLicense(r) - if license == nil { - err := errors.New("no valid license for request") - logger.Error(err) - w.WriteHeader(http.StatusForbidden) - w.Write([]byte(err.Error())) - return - } - - handler(w, r) - } -} - // TODO: move everything below here to a shared package type ErrorResponse struct { diff --git a/pkg/replicatedapp/api.go b/pkg/replicatedapp/api.go index d4e14c59be..710e4de1d5 100644 --- a/pkg/replicatedapp/api.go +++ b/pkg/replicatedapp/api.go @@ -172,7 +172,7 @@ func getApplicationMetadataFromHost(host string, endpoint string, upstream *url. return respBody, nil } -func SendApplicationMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { +func SendCustomAppMetricsData(license *kotsv1beta1.License, app *apptypes.App, data map[string]interface{}) error { url := fmt.Sprintf("%s/application/custom-metrics", license.Spec.Endpoint) payload := struct {