From e48958f5a02e6c3c10fd07ea0a6052e418c9dcba Mon Sep 17 00:00:00 2001 From: Javier Adriel Date: Wed, 6 Jul 2022 23:11:23 -0500 Subject: [PATCH] Connect marketplace API to microservice (#2130) --- .goreleaser.yml | 2 +- models/mp_integration.go | 3 + operatorapi/embedded_spec.go | 20 +++- operatorapi/marketplace.go | 105 ++++++++++++++---- operatorapi/marketplace_test.go | 45 +++++--- .../operator_api/get_m_p_integration.go | 40 +++++++ .../get_m_p_integration_responses.go | 6 +- pkg/build-constants.go | 1 + swagger-operator.yml | 7 +- 9 files changed, 183 insertions(+), 46 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 75219c22a..95fe6693c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -44,7 +44,7 @@ builds: - --tags=kqueue,operator ldflags: - - -s -w -X github.com/minio/console/pkg.ReleaseTag={{.Tag}} -X github.com/minio/console/pkg.CommitID={{.FullCommit}} -X github.com/minio/console/pkg.Version={{.Version}} -X github.com/minio/console/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/console/pkg.ReleaseTime={{.Date}} + - -s -w -X github.com/minio/console/pkg.ReleaseTag={{.Tag}} -X github.com/minio/console/pkg.CommitID={{.FullCommit}} -X github.com/minio/console/pkg.Version={{.Version}} -X github.com/minio/console/pkg.ShortCommitID={{.ShortCommit}} -X github.com/minio/console/pkg.ReleaseTime={{.Date}} -X github.com/minio/console/pkg.MPSecret={{.Env.MPSECRET}} archives: - diff --git a/models/mp_integration.go b/models/mp_integration.go index c3cad25a0..02791090a 100644 --- a/models/mp_integration.go +++ b/models/mp_integration.go @@ -36,6 +36,9 @@ type MpIntegration struct { // email Email string `json:"email,omitempty"` + + // is in e u + IsInEU bool `json:"isInEU,omitempty"` } // Validate validates this mp integration diff --git a/operatorapi/embedded_spec.go b/operatorapi/embedded_spec.go index 2d59eca9e..2dc8f4008 100644 --- a/operatorapi/embedded_spec.go +++ b/operatorapi/embedded_spec.go @@ -321,7 +321,12 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/mpIntegration" + "type": "object", + "properties": { + "isEmailSet": { + "type": "boolean" + } + } } }, "default": { @@ -3358,6 +3363,9 @@ func init() { "properties": { "email": { "type": "string" + }, + "isInEU": { + "type": "boolean" } } }, @@ -4995,7 +5003,12 @@ func init() { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/mpIntegration" + "type": "object", + "properties": { + "isEmailSet": { + "type": "boolean" + } + } } }, "default": { @@ -8863,6 +8876,9 @@ func init() { "properties": { "email": { "type": "string" + }, + "isInEU": { + "type": "boolean" } } }, diff --git a/operatorapi/marketplace.go b/operatorapi/marketplace.go index ef1af9315..180132f16 100644 --- a/operatorapi/marketplace.go +++ b/operatorapi/marketplace.go @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2021 MinIO, Inc. +// Copyright (c) 2022 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -19,14 +19,21 @@ package operatorapi import ( "context" "fmt" + "io" + "net/http" "os" + "strings" + "time" "github.com/go-openapi/runtime/middleware" + "github.com/golang-jwt/jwt/v4" "github.com/minio/console/cluster" "github.com/minio/console/models" "github.com/minio/console/operatorapi/operations" "github.com/minio/console/operatorapi/operations/operator_api" + "github.com/minio/console/pkg" errors "github.com/minio/console/restapi" + "github.com/minio/pkg/env" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -34,7 +41,11 @@ import ( var ( mpConfigMapDefault = "mp-config" mpConfigMapKey = "MP_CONFIG_KEY" - mpEmail = "email" + mpHostEnvVar = "MP_HOST" + defaultMPHost = "https://marketplace.apps.min.dev" + mpEUHostEnvVar = "MP_EU_HOST" + defaultEUMPHost = "https://marketplace-eu.apps.min.dev" + isMPEmailSet = "isEmailSet" emailNotSetMsg = "Email was not sent in request" ) @@ -56,62 +67,112 @@ func registerMarketplaceHandlers(api *operations.OperatorAPI) { }) } -func getMPIntegrationResponse(session *models.Principal, params operator_api.GetMPIntegrationParams) (*models.MpIntegration, *models.Error) { - if true { // This block will be removed once service to register emails is deployed - return nil, &models.Error{Code: 501} - } +func getMPIntegrationResponse(session *models.Principal, params operator_api.GetMPIntegrationParams) (*operator_api.GetMPIntegrationOKBody, *models.Error) { clientSet, err := cluster.K8sClient(session.STSSessionToken) ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if err != nil { return nil, errors.ErrorWithContext(ctx, err) } - mpEmail, err := getMPEmail(ctx, &k8sClient{client: clientSet}) + isMPEmailSet, err := getMPEmail(ctx, &k8sClient{client: clientSet}) if err != nil { return nil, errors.ErrorWithContext(ctx, errors.ErrNotFound) } - return &models.MpIntegration{ - Email: mpEmail, + return &operator_api.GetMPIntegrationOKBody{ + IsEmailSet: isMPEmailSet, }, nil } -func getMPEmail(ctx context.Context, clientSet K8sClientI) (string, error) { +func getMPEmail(ctx context.Context, clientSet K8sClientI) (bool, error) { cm, err := clientSet.getConfigMap(ctx, "default", getMPConfigMapKey(mpConfigMapKey), metav1.GetOptions{}) if err != nil { - return "", err + return false, err } - return cm.Data[mpEmail], nil + return cm.Data[isMPEmailSet] == "true", nil } func postMPIntegrationResponse(session *models.Principal, params operator_api.PostMPIntegrationParams) *models.Error { - if true { // This block will be removed once service to register emails is deployed - return &models.Error{Code: 501} - } clientSet, err := cluster.K8sClient(session.STSSessionToken) ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() if err != nil { return errors.ErrorWithContext(ctx, err) } - return setMPIntegration(ctx, params.Body.Email, &k8sClient{client: clientSet}) + return setMPIntegration(ctx, params.Body.Email, params.Body.IsInEU, &k8sClient{client: clientSet}) } -func setMPIntegration(ctx context.Context, email string, clientSet K8sClientI) *models.Error { +func setMPIntegration(ctx context.Context, email string, isInEU bool, clientSet K8sClientI) *models.Error { if email == "" { return errors.ErrorWithContext(ctx, errors.ErrBadRequest, fmt.Errorf(emailNotSetMsg)) } - if _, err := setMPEmail(ctx, email, clientSet); err != nil { + if _, err := setMPEmail(ctx, email, isInEU, clientSet); err != nil { return errors.ErrorWithContext(ctx, err) } return nil } -func setMPEmail(ctx context.Context, email string, clientSet K8sClientI) (*corev1.ConfigMap, error) { - cm := createCM(email) +func setMPEmail(ctx context.Context, email string, isInEU bool, clientSet K8sClientI) (*corev1.ConfigMap, error) { + if err := postEmailToMP(email, isInEU); err != nil { + return nil, err + } + cm := createCM() return clientSet.createConfigMap(ctx, "default", cm, metav1.CreateOptions{}) } -func createCM(email string) *corev1.ConfigMap { +func postEmailToMP(email string, isInEU bool) error { + mpURL, err := getMPURL(isInEU) + if err != nil { + return err + } + return makePostRequestToMP(mpURL, email) +} + +func getMPURL(isInEU bool) (string, error) { + mpHost := getMPHost(isInEU) + if mpHost == "" { + return "", fmt.Errorf("mp host not set") + } + return fmt.Sprintf("%s/mp-email", mpHost), nil +} + +func getMPHost(isInEU bool) string { + if isInEU { + return env.Get(mpEUHostEnvVar, defaultEUMPHost) + } + return env.Get(mpHostEnvVar, defaultMPHost) +} + +func makePostRequestToMP(url, email string) error { + request, err := createMPRequest(url, email) + if err != nil { + return err + } + client := &http.Client{Timeout: 3 * time.Second} + if res, err := client.Do(request); err != nil { + return err + } else if res.StatusCode >= http.StatusBadRequest { + b, _ := io.ReadAll(res.Body) + return fmt.Errorf("request to %s failed with status code %d and error %s", url, res.StatusCode, string(b)) + } + return nil +} + +func createMPRequest(url, email string) (*http.Request, error) { + request, err := http.NewRequest("POST", url, strings.NewReader(fmt.Sprintf("{\"email\":\"%s\"}", email))) + if err != nil { + return nil, err + } + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{}) + jwtTokenString, err := jwtToken.SignedString([]byte(pkg.MPSecret)) + if err != nil { + return nil, err + } + request.Header.Add("Cookie", fmt.Sprintf("jwtToken=%s", jwtTokenString)) + request.Header.Add("Content-Type", "application/json") + return request, nil +} + +func createCM() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", @@ -121,7 +182,7 @@ func createCM(email string) *corev1.ConfigMap { Name: getMPConfigMapKey(mpConfigMapKey), Namespace: "default", }, - Data: map[string]string{mpEmail: email}, + Data: map[string]string{isMPEmailSet: "true"}, } } diff --git a/operatorapi/marketplace_test.go b/operatorapi/marketplace_test.go index ad7b6ab58..3cc728d7d 100644 --- a/operatorapi/marketplace_test.go +++ b/operatorapi/marketplace_test.go @@ -82,7 +82,7 @@ func (suite *MarketplaceTestSuite) getConfigMapMock(ctx context.Context, namespa if testWithError { return nil, errMock } - return &corev1.ConfigMap{Data: map[string]string{mpEmail: "mock@mock.com"}}, nil + return &corev1.ConfigMap{Data: map[string]string{isMPEmailSet: "true"}}, nil } func (suite *MarketplaceTestSuite) createConfigMapMock(ctx context.Context, namespace string, cm *corev1.ConfigMap, opts metav1.CreateOptions) (*corev1.ConfigMap, error) { @@ -115,11 +115,6 @@ func (suite *MarketplaceTestSuite) TestRegisterMarketplaceHandlers() { suite.assert.NotNil(api.OperatorAPIPostMPIntegrationHandler) } -// TODO -// WIP - Complete successful tests to RUD handlers -// WIP - Add tests to POST handler -// WIP - Check how to mock k8s objects for tests with no error - func (suite *MarketplaceTestSuite) TestGetMPIntegrationHandlerWithError() { api := &operations.OperatorAPI{} registerMarketplaceHandlers(api) @@ -130,6 +125,19 @@ func (suite *MarketplaceTestSuite) TestGetMPIntegrationHandlerWithError() { suite.assert.True(ok) } +func (suite *MarketplaceTestSuite) TestPostMPIntegrationHandlerWithError() { + api := &operations.OperatorAPI{} + registerMarketplaceHandlers(api) + params := operator_api.NewPostMPIntegrationParams() + params.Body = &models.MpIntegration{Email: ""} + params.HTTPRequest = &http.Request{} + params.HTTPRequest.Header = map[string][]string{} + params.HTTPRequest.AddCookie(&http.Cookie{Value: "token", Name: "token"}) + response := api.OperatorAPIPostMPIntegrationHandler.Handle(params, &models.Principal{}) + _, ok := response.(*operator_api.PostMPIntegrationDefault) + suite.assert.True(ok) +} + func (suite *MarketplaceTestSuite) TestGetMPEmailWithError() { testWithError = true ctx, cancel := context.WithCancel(context.Background()) @@ -143,15 +151,15 @@ func (suite *MarketplaceTestSuite) TestGetMPEmailNoError() { testWithError = false ctx, cancel := context.WithCancel(context.Background()) defer cancel() - email, err := getMPEmail(ctx, &suite.kClient) + isSet, err := getMPEmail(ctx, &suite.kClient) suite.assert.Nil(err) - suite.assert.NotEmpty(email) + suite.assert.True(isSet) } func (suite *MarketplaceTestSuite) TestSetMPIntegrationNoEmail() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - err := setMPIntegration(ctx, "", &suite.kClient) + err := setMPIntegration(ctx, "", false, &suite.kClient) suite.assert.NotNil(err) } @@ -159,17 +167,20 @@ func (suite *MarketplaceTestSuite) TestSetMPIntegrationWithError() { testWithError = true ctx, cancel := context.WithCancel(context.Background()) defer cancel() - err := setMPIntegration(ctx, "mock@mock.com", &suite.kClient) + os.Setenv(mpHostEnvVar, " ") + err := setMPIntegration(ctx, "mock@mock.com", false, &suite.kClient) suite.assert.NotNil(err) + os.Unsetenv(mpHostEnvVar) } -func (suite *MarketplaceTestSuite) TestSetMPIntegrationNoError() { - testWithError = false - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - err := setMPIntegration(ctx, "mock@mock.com", &suite.kClient) - suite.assert.Nil(err) -} +// TODO: Add mock server for testing microservice +// func (suite *MarketplaceTestSuite) TestSetMPIntegrationNoError() { +// testWithError = false +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// err := setMPIntegration(ctx, "mock@mock.com", "token", &suite.kClient) +// suite.assert.Nil(err) +// } func TestMarketplace(t *testing.T) { suite.Run(t, new(MarketplaceTestSuite)) diff --git a/operatorapi/operations/operator_api/get_m_p_integration.go b/operatorapi/operations/operator_api/get_m_p_integration.go index b6b3d9d84..30a2c45c9 100644 --- a/operatorapi/operations/operator_api/get_m_p_integration.go +++ b/operatorapi/operations/operator_api/get_m_p_integration.go @@ -23,9 +23,12 @@ package operator_api // Editing this file might prove futile when you re-run the generate command import ( + "context" "net/http" "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/minio/console/models" ) @@ -86,3 +89,40 @@ func (o *GetMPIntegration) ServeHTTP(rw http.ResponseWriter, r *http.Request) { o.Context.Respond(rw, r, route.Produces, route, res) } + +// GetMPIntegrationOKBody get m p integration o k body +// +// swagger:model GetMPIntegrationOKBody +type GetMPIntegrationOKBody struct { + + // is email set + IsEmailSet bool `json:"isEmailSet,omitempty"` +} + +// Validate validates this get m p integration o k body +func (o *GetMPIntegrationOKBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this get m p integration o k body based on context it is used +func (o *GetMPIntegrationOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *GetMPIntegrationOKBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *GetMPIntegrationOKBody) UnmarshalBinary(b []byte) error { + var res GetMPIntegrationOKBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} diff --git a/operatorapi/operations/operator_api/get_m_p_integration_responses.go b/operatorapi/operations/operator_api/get_m_p_integration_responses.go index c20e6c8ee..7a5d933c0 100644 --- a/operatorapi/operations/operator_api/get_m_p_integration_responses.go +++ b/operatorapi/operations/operator_api/get_m_p_integration_responses.go @@ -42,7 +42,7 @@ type GetMPIntegrationOK struct { /* In: Body */ - Payload *models.MpIntegration `json:"body,omitempty"` + Payload *GetMPIntegrationOKBody `json:"body,omitempty"` } // NewGetMPIntegrationOK creates GetMPIntegrationOK with default headers values @@ -52,13 +52,13 @@ func NewGetMPIntegrationOK() *GetMPIntegrationOK { } // WithPayload adds the payload to the get m p integration o k response -func (o *GetMPIntegrationOK) WithPayload(payload *models.MpIntegration) *GetMPIntegrationOK { +func (o *GetMPIntegrationOK) WithPayload(payload *GetMPIntegrationOKBody) *GetMPIntegrationOK { o.Payload = payload return o } // SetPayload sets the payload to the get m p integration o k response -func (o *GetMPIntegrationOK) SetPayload(payload *models.MpIntegration) { +func (o *GetMPIntegrationOK) SetPayload(payload *GetMPIntegrationOKBody) { o.Payload = payload } diff --git a/pkg/build-constants.go b/pkg/build-constants.go index 360a8bb01..739c3fe51 100644 --- a/pkg/build-constants.go +++ b/pkg/build-constants.go @@ -27,4 +27,5 @@ var ( CommitID = "(dev)" // ShortCommitID - first 12 characters from CommitID. ShortCommitID = "(dev)" + MPSecret = "(dev)" ) diff --git a/swagger-operator.yml b/swagger-operator.yml index 7f14f4838..59bc7bba2 100644 --- a/swagger-operator.yml +++ b/swagger-operator.yml @@ -1240,7 +1240,10 @@ paths: 200: description: A successful response. schema: - $ref: "#/definitions/mpIntegration" + type: object + properties: + isEmailSet: + type: boolean default: description: Generic error response. schema: @@ -3394,4 +3397,6 @@ definitions: properties: email: type: string + isInEU: + type: boolean