Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an endpoint to KOTS where application metrics can be retrieved adhoc #4060

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading