From df666bd93b4e3ee4d01915b62cc58e89eab12068 Mon Sep 17 00:00:00 2001 From: Piotr Halama Date: Thu, 22 Feb 2024 14:00:55 +0100 Subject: [PATCH] kyma provision --- internal/btp/provision.go | 87 +++++++++++++++++ internal/btp/provision_test.go | 142 ++++++++++++++++++++++++++++ internal/cmd/provision/provision.go | 35 +++++-- 3 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 internal/btp/provision.go create mode 100644 internal/btp/provision_test.go diff --git a/internal/btp/provision.go b/internal/btp/provision.go new file mode 100644 index 000000000..d65c33f40 --- /dev/null +++ b/internal/btp/provision.go @@ -0,0 +1,87 @@ +package btp + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +const provisionEndpoint = "provisioning/v1/environments" + +type ProvisionEnvironment struct { + // Description string `json:"description,omitempty"` + EnvironmentType string `json:"environmentType"` + // LandscapeLabel string `json:"landscapeLabel,omitempty"` + Name string `json:"name"` + // Origin string `json:"origin,omitempty"` + Parameters KymaParameters `json:"parameters"` + PlanName string `json:"planName"` + // ServiceName string `json:"serviceName,omitempty"` + // TechnicalKey string `json:"technicalKey,omitempty"` + User string `json:"user"` +} + +type KymaParameters struct { + Name string `json:"name"` + Region string `json:"region"` +} + +type ProvisionResponse struct { + ID string `json:"id"` + Name string `json:"name"` + BrokerID string `json:"brokerId"` + GlobalAccountGUID string `json:"globalAccountGUID"` + SubaccountGUID string `json:"subaccountGUID"` + TenantID string `json:"tenantId"` + ServiceID string `json:"serviceId"` + PlanID string `json:"planId"` + DashboardURL string `json:"dashboardUrl"` + Operation string `json:"operation"` + Parameters string `json:"parameters"` + Labels string `json:"labels"` + // CustomLabels struct {} `json:"customLabels"` + Type string `json:"type"` + Status string `json:"status"` + EnvironmentType string `json:"environmentType"` + PlatformID string `json:"platformId"` + CreatedDate int64 `json:"createdDate"` + ModifiedDate int64 `json:"modifiedDate"` + State string `json:"state"` + StateMessage string `json:"stateMessage"` + ServiceName string `json:"serviceName"` + PlanName string `json:"planName"` +} + +func (c *LocalClient) Provision(pe *ProvisionEnvironment) (*ProvisionResponse, error) { + reqData, err := json.Marshal(pe) + if err != nil { + return nil, err + } + + provisionURL := fmt.Sprintf("%s/%s", c.credentials.Endpoints.ProvisioningServiceURL, provisionEndpoint) + options := requestOptions{ + Body: bytes.NewBuffer(reqData), + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + + response, err := c.cis.post(provisionURL, options) + if err != nil { + return nil, fmt.Errorf("failed to provision: %s", err.Error()) + } + defer response.Body.Close() + + return decodeProvisionSuccessResponse(response) +} + +func decodeProvisionSuccessResponse(response *http.Response) (*ProvisionResponse, error) { + provisionResponse := ProvisionResponse{} + err := json.NewDecoder(response.Body).Decode(&provisionResponse) + if err != nil { + return nil, fmt.Errorf("failed to decode response: %s", err.Error()) + } + + return &provisionResponse, nil +} diff --git a/internal/btp/provision_test.go b/internal/btp/provision_test.go new file mode 100644 index 000000000..c32d22f73 --- /dev/null +++ b/internal/btp/provision_test.go @@ -0,0 +1,142 @@ +package btp + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func fixProvisionHandler(t *testing.T) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != fmt.Sprintf("/%s", provisionEndpoint) { + w.WriteHeader(404) + return + } + request := ProvisionEnvironment{} + err := json.NewDecoder(r.Body).Decode(&request) + require.NoError(t, err) + + resp := ProvisionResponse{ + Name: request.Name, + } + + data, err := json.Marshal(resp) + require.NoError(t, err) + + w.WriteHeader(202) + _, err = w.Write(data) + require.NoError(t, err) + } +} + +func fixProvisionErrorHandler(t *testing.T) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, _ *http.Request) { + data := cisErrorResponse{ + Error: cisError{ + Message: "error", + }, + } + response, err := json.Marshal(data) + require.NoError(t, err) + + w.WriteHeader(401) + _, err = w.Write(response) + require.NoError(t, err) + } +} + +func TestCISClient_Provision(t *testing.T) { + t.Parallel() + + svrGood := httptest.NewServer(http.HandlerFunc(fixProvisionHandler(t))) + defer svrGood.Close() + svrBad := httptest.NewServer(http.HandlerFunc(fixProvisionErrorHandler(t))) + defer svrBad.Close() + + tests := []struct { + name string + credentials *CISCredentials + token *XSUAAToken + pe *ProvisionEnvironment + wantedResponse *ProvisionResponse + expectedErr error + }{ + { + name: "Correct data", + credentials: &CISCredentials{ + Endpoints: Endpoints{ + ProvisioningServiceURL: svrGood.URL, + }, + }, + token: &XSUAAToken{}, + pe: &ProvisionEnvironment{ + Name: "name", + }, + wantedResponse: &ProvisionResponse{ + Name: "name", + }, + expectedErr: nil, + }, + { + name: "Incorrect URL", + credentials: &CISCredentials{ + Endpoints: Endpoints{ + ProvisioningServiceURL: "?\n?", + }, + }, + token: &XSUAAToken{}, + pe: &ProvisionEnvironment{ + Name: "name", + }, + wantedResponse: nil, + expectedErr: errors.New("failed to provision: failed to build request: parse \"?\\n?/provisioning/v1/environments\": net/url: invalid control character in URL"), + }, + { + name: "Wrong URL", + credentials: &CISCredentials{ + Endpoints: Endpoints{ + ProvisioningServiceURL: "http://doesnotexist", + }, + }, + token: &XSUAAToken{}, + pe: &ProvisionEnvironment{ + Name: "name", + }, + wantedResponse: nil, + expectedErr: errors.New("failed to provision: failed to get data from server: Post \"http://doesnotexist/provisioning/v1/environments\": dial tcp: lookup doesnotexist: no such host"), + }, + { + name: "Error response", + credentials: &CISCredentials{ + Endpoints: Endpoints{ + ProvisioningServiceURL: svrBad.URL, + }, + }, + token: &XSUAAToken{}, + pe: &ProvisionEnvironment{ + Name: "name", + }, + wantedResponse: nil, + expectedErr: errors.New("failed to provision: error"), + }, + } + for _, tt := range tests { + pe := tt.pe + credentials := tt.credentials + token := tt.token + wantedResponse := tt.wantedResponse + expectedErr := tt.expectedErr + t.Run(tt.name, func(t *testing.T) { + c := NewLocalClient(credentials, token) + + response, err := c.Provision(pe) + require.Equal(t, expectedErr, err) + require.Equal(t, wantedResponse, response) + }) + } +} diff --git a/internal/cmd/provision/provision.go b/internal/cmd/provision/provision.go index 6b282c9f4..93388f27b 100644 --- a/internal/cmd/provision/provision.go +++ b/internal/cmd/provision/provision.go @@ -1,7 +1,6 @@ package provision import ( - "encoding/json" "fmt" "github.com/kyma-project/cli.v3/internal/btp" @@ -10,13 +9,20 @@ import ( type provisionConfig struct { credentialsPath string + plan string + environmentName string + clusterName string + region string } func NewProvisionCMD() *cobra.Command { config := provisionConfig{} cmd := &cobra.Command{ - Use: "provision", + Use: "provision", + Short: "Provisions a Kyma cluster on the BTP.", + Long: `Use this command to provision a Kyma environment on the SAP BTP platform. +`, RunE: func(_ *cobra.Command, _ []string) error { return runProvision(&config) }, @@ -24,6 +30,12 @@ func NewProvisionCMD() *cobra.Command { cmd.PersistentFlags() cmd.Flags().StringVar(&config.credentialsPath, "credentials-path", "", "Path to the CIS credentials file.") + + cmd.Flags().StringVar(&config.plan, "plan", "trial", "Name of the Kyma environment plan, e.g trial, azure, aws, gcp.") + cmd.Flags().StringVar(&config.environmentName, "environment-name", "kyma", "Name of the environment in the BTP.") + cmd.Flags().StringVar(&config.clusterName, "cluster-name", "kyma", "Name of the Kyma cluster.") + cmd.Flags().StringVar(&config.region, "region", "", "Name of the region of the Kyma cluster.") + _ = cmd.MarkFlagRequired("credentials-path") return cmd @@ -40,13 +52,24 @@ func runProvision(config *provisionConfig) error { return fmt.Errorf("failed to get access token: %s", err.Error()) } - // TODO: remove in next interation - data, err := json.Marshal(token) + localCISClient := btp.NewLocalClient(credentials, token) + + ProvisionEnvironment := &btp.ProvisionEnvironment{ + EnvironmentType: "kyma", + PlanName: config.plan, + Name: config.environmentName, + User: "kyma-cli", + Parameters: btp.KymaParameters{ + Name: config.clusterName, + Region: config.region, + }, + } + response, err := localCISClient.Provision(ProvisionEnvironment) if err != nil { - return err + return fmt.Errorf("failed to provision kyma runtime: %s", err.Error()) } - fmt.Printf("%s\n", data) + fmt.Printf("Kyma environment provisioning, environment name: '%s', id: '%s'\n", response.Name, response.ID) return nil }