Skip to content

Commit

Permalink
Add an endpoint to KOTS where application metrics can be retrieved ad…
Browse files Browse the repository at this point in the history
…hoc (#4060)

* Add an endpoint to KOTS where application metrics can be retrieved adhoc
  • Loading branch information
sgalsaleh authored Oct 9, 2023
1 parent d0ba98c commit 5705b14
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 58 deletions.
6 changes: 6 additions & 0 deletions pkg/apiserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
**********************************************************************/
Expand Down
8 changes: 7 additions & 1 deletion pkg/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,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{}) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/handlers/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,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)
}
26 changes: 18 additions & 8 deletions pkg/handlers/custom_metrics.go → pkg/handlers/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@ import (
"github.com/replicatedhq/kots/pkg/kotsutil"
"github.com/replicatedhq/kots/pkg/logger"
"github.com/replicatedhq/kots/pkg/replicatedapp"
"github.com/replicatedhq/kots/pkg/reporting"
"github.com/replicatedhq/kots/pkg/session"
"github.com/replicatedhq/kots/pkg/store"
)

type SendCustomApplicationMetricsRequest struct {
Data ApplicationMetricsData `json:"data"`
func (h *Handler) GetAppMetrics(w http.ResponseWriter, r *http.Request) {
app := session.ContextGetApp(r)
reportingInfo := reporting.GetReportingInfo(app.ID)
headers := reporting.GetReportingInfoHeaders(reportingInfo)

JSON(w, http.StatusOK, headers)
}

type SendCustomAppMetricsRequest struct {
Data CustomAppMetricsData `json:"data"`
}

type ApplicationMetricsData map[string]interface{}
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)
Expand Down Expand Up @@ -48,20 +58,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)
Expand All @@ -72,7 +82,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")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import (
"github.com/stretchr/testify/require"
)

func Test_validateCustomMetricsData(t *testing.T) {
func Test_validateCustomAppMetricsData(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,
Expand All @@ -34,28 +34,28 @@ 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"},
},
wantErr: true,
},
{
name: "map value",
data: ApplicationMetricsData{
data: CustomAppMetricsData{
"key1": 10,
"key2": map[string]string{"key1": "val1"},
},
wantErr: true,
},
{
name: "nil value",
data: ApplicationMetricsData{
data: CustomAppMetricsData{
"key1": nil,
"key2": 4,
},
Expand All @@ -65,7 +65,7 @@ func Test_validateCustomMetricsData(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateCustomMetricsData(test.data)
err := validateCustomAppMetricsData(test.data)
if test.wantErr {
require.Error(t, err)
} else {
Expand All @@ -75,7 +75,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"
Expand Down Expand Up @@ -122,7 +122,7 @@ spec:

// Validate

handler.GetSendCustomApplicationMetricsHandler(mockStore)(clientWriter, clientRequest)
handler.GetSendCustomAppMetricsHandler(mockStore)(clientWriter, clientRequest)

req.Equal(http.StatusOK, clientWriter.Code)
}
18 changes: 18 additions & 0 deletions pkg/handlers/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
12 changes: 12 additions & 0 deletions pkg/handlers/mock/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions pkg/handlers/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
15 changes: 0 additions & 15 deletions pkg/policy/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/replicatedapp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 5705b14

Please sign in to comment.