diff --git a/go.mod b/go.mod index 9643257..a035c1f 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,15 @@ go 1.21 require ( github.com/antihax/optional v1.0.0 github.com/golang/mock v1.6.0 + github.com/stretchr/testify v1.9.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/tools v0.1.4 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3762b4f..2ded73e 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -31,3 +37,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/client/api.go b/pkg/client/api.go index ef21d67..f8e5ce0 100644 --- a/pkg/client/api.go +++ b/pkg/client/api.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package client @@ -25,6 +25,8 @@ type api struct { jsonParser jsonPareserFunc validations []validationFunc compatibleVersion string + // removeVmaasCMPBasePath is used to remove the base path of the vmaas-cmp API, for use by the broker API + removeVmaasCMPBasePath bool } // do will call the API provided. this function will not return any response, but @@ -49,7 +51,15 @@ func (a *api) do(ctx context.Context, request interface{}, queryParams map[strin if a.path == "" || a.method == "" || a.client == nil || a.jsonParser == nil { panic("api not properly configured") } - a.path = fmt.Sprintf("%s/%s/%s", a.client.getHost(), consts.VmaasCmpAPIBasePath, a.path) + + // Set the path + if !a.removeVmaasCMPBasePath { + // Add the base path of the vmaas-cmp API if we are calling the vmaas-cmp API + a.path = fmt.Sprintf("%s/%s/%s", a.client.getHost(), consts.VmaasCmpAPIBasePath, a.path) + } else { + // Don't use the base path of the vmaas-cmp API if we are calling the broker API + a.path = fmt.Sprintf("%s/%s", a.client.getHost(), a.path) + } for _, validations := range a.validations { err := validations() diff --git a/pkg/client/broker.go b/pkg/client/broker.go new file mode 100644 index 0000000..84c1804 --- /dev/null +++ b/pkg/client/broker.go @@ -0,0 +1,68 @@ +// (C) Copyright 2024 Hewlett Packard Enterprise Development LP + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + consts "github.com/HewlettPackard/hpegl-vmaas-cmp-go-sdk/pkg/common" + "github.com/HewlettPackard/hpegl-vmaas-cmp-go-sdk/pkg/models" +) + +// BrokerAPIService is a service that provides methods to interact with the broker API +type BrokerAPIService struct { + Client APIClientHandler + Cfg Configuration +} + +// GetMorpheusDetails returns Morpheus details to terraform +func (a *BrokerAPIService) GetMorpheusDetails(ctx context.Context) (models.TFMorpheusDetails, error) { + // Get the service instance ID and Morpheus URL + ServiceSubscriptionDetailsResp := models.SubscriptionDetailsResponse{} + serviceSubscriptionDetailsAPI := &api{ + method: http.MethodGet, + path: consts.SubscriptionDetails, + client: a.Client, + removeVmaasCMPBasePath: true, + + jsonParser: func(body []byte) error { + return json.Unmarshal(body, &ServiceSubscriptionDetailsResp) + }, + } + + // Use the default query params + if err := serviceSubscriptionDetailsAPI.do(ctx, nil, a.Cfg.DefaultQueryParams); err != nil { + return models.TFMorpheusDetails{}, fmt.Errorf("error getting service subscription details: %v", err) + } + + // Get the Morpheus token + MorpheusTokenResp := models.MorpheusTokenResponse{} + morpheusTokenAPI := &api{ + method: http.MethodGet, + path: fmt.Sprintf(consts.MorpheusToken, ServiceSubscriptionDetailsResp.ServiceInstanceID), + client: a.Client, + removeVmaasCMPBasePath: true, + + jsonParser: func(body []byte) error { + return json.Unmarshal(body, &MorpheusTokenResp) + }, + } + + // No query params needed + if err := morpheusTokenAPI.do(ctx, nil, nil); err != nil { + return models.TFMorpheusDetails{}, fmt.Errorf("error getting Morpheus token: %v", err) + } + + // build response + ret := models.TFMorpheusDetails{ + ID: ServiceSubscriptionDetailsResp.ServiceInstanceID, + AccessToken: MorpheusTokenResp.AccessToken, + ValidTill: MorpheusTokenResp.Expires, + URL: ServiceSubscriptionDetailsResp.URL, + } + + return ret, nil +} diff --git a/pkg/client/broker_test.go b/pkg/client/broker_test.go new file mode 100644 index 0000000..010469d --- /dev/null +++ b/pkg/client/broker_test.go @@ -0,0 +1,264 @@ +// (C) Copyright 2024 Hewlett Packard Enterprise Development LP + +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "testing" + + "github.com/golang/mock/gomock" + + "github.com/stretchr/testify/assert" + + consts "github.com/HewlettPackard/hpegl-vmaas-cmp-go-sdk/pkg/common" + "github.com/HewlettPackard/hpegl-vmaas-cmp-go-sdk/pkg/models" +) + +const ( + testServiceInstanceID = "18ba6409-ac59-4eac-9414-0147e72d615e" + testAccessToken = "2b9fba7f-7c14-4773-a970-a9ad393811ac" + testRefreshToken = "7806acfb-f847-48b1-a6d5-6119dccb3ffe" + testMorpheusURL = "https://1234-mp.private.greenlake.hpe-gl-intg.com/" + testAccessTokenExpires = 1758034360176 + testAccessTokenExpiresIn = 3600 +) + +func TestBrokerAPIService_GetMorpheusDetails(t *testing.T) { + ctx := context.Background() + testCtrl := gomock.NewController(t) + defer testCtrl.Finish() + + headers := getDefaultHeaders() + + queryParams := map[string]string{ + "location": "BLR", + "space_name": "default", + } + + clientCfg := Configuration{ + DefaultHeader: headers, + DefaultQueryParams: queryParams, + } + + tests := []struct { + name string + given func(m *MockAPIClientHandler) + want models.TFMorpheusDetails + wantErr bool + }{ + { + name: "Test GetMorpheusDetails success", + want: models.TFMorpheusDetails{ + ID: testServiceInstanceID, + AccessToken: testAccessToken, + ValidTill: testAccessTokenExpires, + URL: testMorpheusURL, + }, + wantErr: false, + given: func(m *MockAPIClientHandler) { + // Get subscription details + m.EXPECT().getHost().Return(mockHost) + pathSubscription := mockHost + "/" + consts.SubscriptionDetails + method := "GET" + reqSubscription, _ := http.NewRequest(method, pathSubscription, nil) + respBodySubscription := io.NopCloser(bytes.NewReader([]byte(` + { + "ServiceInstanceID": "` + testServiceInstanceID + `", + "URL": "` + testMorpheusURL + `" + } + `))) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathSubscription, method, nil, headers, + getURLValues(queryParams), url.Values{}, "", nil).Return(reqSubscription, nil) + + m.EXPECT().callAPI(reqSubscription).Return(&http.Response{ + StatusCode: 200, + Body: respBodySubscription, + }, nil) + + // Get Morpheus token + m.EXPECT().getHost().Return(mockHost) + pathToken := mockHost + "/" + fmt.Sprintf(consts.MorpheusToken, testServiceInstanceID) + reqToken, _ := http.NewRequest(method, pathToken, nil) + tokenResp := models.MorpheusTokenResponse{ + AccessToken: testAccessToken, + Expires: testAccessTokenExpires, + RefreshToken: testRefreshToken, + ExpiresIn: testAccessTokenExpiresIn, + } + body, err := json.Marshal(tokenResp) + assert.NoError(t, err) + respBodyToken := io.NopCloser(bytes.NewReader(body)) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathToken, method, nil, headers, + url.Values{}, url.Values{}, "", nil).Return(reqToken, nil) + + m.EXPECT().callAPI(reqToken).Return(&http.Response{ + StatusCode: 200, + Body: respBodyToken, + }, nil) + }, + }, + + { + name: "Test GetMorpheusDetails error in get subscription details prepare request", + want: models.TFMorpheusDetails{}, + wantErr: true, + given: func(m *MockAPIClientHandler) { + // Get subscription details + m.EXPECT().getHost().Return(mockHost) + pathSubscription := mockHost + "/" + consts.SubscriptionDetails + method := "GET" + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathSubscription, method, nil, headers, + getURLValues(queryParams), url.Values{}, "", nil). + Return(nil, errors.New("error in prepare request")) + }, + }, + + { + name: "Test GetMorpheusDetails error in get subscription details call API", + want: models.TFMorpheusDetails{}, + wantErr: true, + given: func(m *MockAPIClientHandler) { + // Get subscription details + m.EXPECT().getHost().Return(mockHost) + pathSubscription := mockHost + "/" + consts.SubscriptionDetails + method := "GET" + reqSubscription, _ := http.NewRequest(method, pathSubscription, nil) + respBodySubscription := io.NopCloser(bytes.NewReader([]byte(` + { + "ServiceInstanceID": "` + testServiceInstanceID + `", + "URL": "` + testMorpheusURL + `" + } + `))) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathSubscription, method, nil, headers, + getURLValues(queryParams), url.Values{}, "", nil).Return(reqSubscription, nil) + + m.EXPECT().callAPI(reqSubscription).Return(&http.Response{ + StatusCode: 500, + Body: respBodySubscription, + }, nil) + }, + }, + + { + name: "Test GetMorpheusDetails error in get Morpheus token prepare request", + want: models.TFMorpheusDetails{}, + wantErr: true, + given: func(m *MockAPIClientHandler) { + // Get subscription details + m.EXPECT().getHost().Return(mockHost) + pathSubscription := mockHost + "/" + consts.SubscriptionDetails + method := "GET" + reqSubscription, _ := http.NewRequest(method, pathSubscription, nil) + respBodySubscription := io.NopCloser(bytes.NewReader([]byte(` + { + "ServiceInstanceID": "` + testServiceInstanceID + `", + "URL": "` + testMorpheusURL + `" + } + `))) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathSubscription, method, nil, headers, + getURLValues(queryParams), url.Values{}, "", nil).Return(reqSubscription, nil) + + m.EXPECT().callAPI(reqSubscription).Return(&http.Response{ + StatusCode: 200, + Body: respBodySubscription, + }, nil) + + // Get Morpheus token + m.EXPECT().getHost().Return(mockHost) + pathToken := mockHost + "/" + fmt.Sprintf(consts.MorpheusToken, testServiceInstanceID) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathToken, method, nil, headers, + url.Values{}, url.Values{}, "", nil). + Return(nil, errors.New("error in prepare request")) + }, + }, + + { + name: "Test GetMorpheusDetails error in get Morpheus token call API", + want: models.TFMorpheusDetails{}, + wantErr: true, + given: func(m *MockAPIClientHandler) { + // Get subscription details + m.EXPECT().getHost().Return(mockHost) + pathSubscription := mockHost + "/" + consts.SubscriptionDetails + method := "GET" + reqSubscription, _ := http.NewRequest(method, pathSubscription, nil) + respBodySubscription := io.NopCloser(bytes.NewReader([]byte(` + { + "ServiceInstanceID": "` + testServiceInstanceID + `", + "URL": "` + testMorpheusURL + `" + } + `))) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathSubscription, method, nil, headers, + getURLValues(queryParams), url.Values{}, "", nil).Return(reqSubscription, nil) + + m.EXPECT().callAPI(reqSubscription).Return(&http.Response{ + StatusCode: 200, + Body: respBodySubscription, + }, nil) + + // Get Morpheus token + m.EXPECT().getHost().Return(mockHost) + pathToken := mockHost + "/" + fmt.Sprintf(consts.MorpheusToken, testServiceInstanceID) + reqToken, _ := http.NewRequest(method, pathToken, nil) + tokenResp := models.MorpheusTokenResponse{ + AccessToken: testAccessToken, + Expires: testAccessTokenExpires, + RefreshToken: testRefreshToken, + ExpiresIn: testAccessTokenExpiresIn, + } + body, err := json.Marshal(tokenResp) + assert.NoError(t, err) + respBodyToken := io.NopCloser(bytes.NewReader([]byte(body))) + // mock the context only since it is not validated in this function + m.EXPECT().getVersion().Return(999999) + m.EXPECT().prepareRequest(gomock.Any(), pathToken, method, nil, headers, + url.Values{}, url.Values{}, "", nil).Return(reqToken, nil) + + m.EXPECT().callAPI(reqToken).Return(&http.Response{ + StatusCode: 500, + Body: respBodyToken, + }, nil) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := NewMockAPIClientHandler(testCtrl) + tt.given(mockClient) + a := &BrokerAPIService{ + Cfg: clientCfg, + Client: mockClient, + } + got, err := a.GetMorpheusDetails(ctx) + if (err != nil) != tt.wantErr { + t.Errorf("BrokerAPIService.GetMorpheusDetails() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("BrokerAPIService.GetMorpheusDetails() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 7e5a514..89c6d1f 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP //go:generate go run github.com/golang/mock/mockgen -source ./client.go -package client -destination ./client_mock.go @@ -36,6 +36,11 @@ type APIClientHandler interface { SetMeta(meta interface{}, fn SetScmClientToken) error getVersion() int getHost() string + // The next two methods are for use when creating the Broker API client + // SetMetaFnAndVersion is used to set the client token function in meta and the SCM version for the Broker client + SetMetaFnAndVersion(meta interface{}, version int, fn SetScmClientToken) + // GetSCMVersion returns the SCM version for use when creating the Broker client + GetSCMVersion() int } // APIClient manages communication with the GreenLake Private Cloud VMaaS CMP API API v1.0.0 @@ -96,6 +101,12 @@ func (c *APIClient) SetMeta(meta interface{}, fn SetScmClientToken) error { return nil } +func (c *APIClient) SetMetaFnAndVersion(meta interface{}, version int, fn SetScmClientToken) { + c.meta = meta + c.tokenFunc = fn + c.cmpVersion = version +} + // callAPI do the request. func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { return c.cfg.HTTPClient.Do(request) @@ -110,8 +121,12 @@ func (c *APIClient) getVersion() int { return c.cmpVersion } +func (c *APIClient) GetSCMVersion() int { + return c.cmpVersion +} + // prepareRequest build the request -//nolint +// nolint func (c *APIClient) prepareRequest( ctx context.Context, path string, method string, diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 84df06a..c09e85e 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -69,4 +69,8 @@ const ( // Morpheus version CMPSixZeroFiveVersion = "6.0.5" + + // Broker API paths + SubscriptionDetails = "vmaas/api/v1alpha1/subscription_details" + MorpheusToken = "vmaas/api/v1/service_instances/%s/cmp_access_token" ) diff --git a/pkg/models/broker.go b/pkg/models/broker.go new file mode 100644 index 0000000..3cdae0e --- /dev/null +++ b/pkg/models/broker.go @@ -0,0 +1,29 @@ +// (C) Copyright 2024 Hewlett Packard Enterprise Development LP + +package models + +// Broker structs go here + +// SubscriptionDetailsResponse is the response for Subscription Details from the broker +type SubscriptionDetailsResponse struct { + ServiceInstanceID string `json:"ServiceInstanceID"` + URL string `json:"URL"` +} + +// MorpheusTokenResponse is the response for Morpheus Token from the broker +type MorpheusTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Expires int64 `json:"expires"` + ExpiresIn int64 `json:"expires_in"` +} + +// TFMorpheusDetails is what we return to terraform +type TFMorpheusDetails struct { + // ID is the ServiceInstanceID, added here for use by the provider when storing the data + ID string `json:"id"` + AccessToken string `json:"access_token"` + // ValidTill Unix timestamp of when the access_token expires in seconds + ValidTill int64 `json:"valid_till"` + URL string `json:"URL"` +}