Skip to content

Commit

Permalink
Added the new Hana Map command that fulfills the new CLI requirements (
Browse files Browse the repository at this point in the history
  • Loading branch information
Cortey authored Nov 12, 2024
1 parent e72df26 commit 336782b
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 0 deletions.
21 changes: 21 additions & 0 deletions internal/btp/hana/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hana

import (
"encoding/json"
"github.com/kyma-project/cli.v3/internal/clierror"
"os"
)

func ReadCredentialsFromFile(path string) (*HanaAdminCredentials, clierror.Error) {
bytes, err := os.ReadFile(path)
if err != nil {
return nil, clierror.Wrap(err, clierror.New("failed to read credentials file"))
}
credentials := &HanaAdminCredentials{}

err = json.Unmarshal(bytes, credentials)
if err != nil {
return nil, clierror.Wrap(err, clierror.New("failed to parse credentials file"))
}
return credentials, nil
}
44 changes: 44 additions & 0 deletions internal/btp/hana/hana.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package hana

import (
"encoding/json"
"fmt"
"github.com/kyma-project/cli.v3/internal/clierror"
"io"
"net/http"
)

func GetID(baseURL, token string) (string, clierror.Error) {
return getID(fmt.Sprintf("https://%s", baseURL), token)
}

func getID(baseURL, token string) (string, clierror.Error) {
client := &http.Client{}
requestGet, err := http.NewRequest("GET", fmt.Sprintf("%s/inventory/v2/serviceInstances", baseURL), nil)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to create a get Hana instances request"))
}
requestGet.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

response, err := client.Do(requestGet)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to get Hana instances"))
}

if response.StatusCode != http.StatusOK {
return "", clierror.New(fmt.Sprintf("unexpected status code: %d", response.StatusCode))
}
hanaInstanceBytes, err := io.ReadAll(response.Body)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to read Hana instances from the response"))
}
hanaInstance := HanaInstance{}
err = json.Unmarshal(hanaInstanceBytes, &hanaInstance)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to parse Hana instances from the response"))
}
if len(hanaInstance.Data) == 0 {
return "", clierror.New("no Hana instances found in the response")
}
return hanaInstance.Data[0].ID, nil
}
76 changes: 76 additions & 0 deletions internal/btp/hana/hana_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package hana

import (
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"testing"
)

func TestGetID(t *testing.T) {
authToken := "test-token"
t.Run("get hana instance ID from server", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/inventory/v2/serviceInstances", r.URL.Path)
require.Equal(t, fmt.Sprintf("Bearer %s", authToken), r.Header.Get("Authorization"))
require.Equal(t, http.MethodGet, r.Method)

_, err := w.Write([]byte(`{"data":[{"id":"test-id"}]}`))
require.NoError(t, err)
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()

id, err := getID(testServer.URL, authToken)
require.Nil(t, err)
require.Equal(t, "test-id", id)
})
t.Run("response body empty", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/inventory/v2/serviceInstances", r.URL.Path)
require.Equal(t, fmt.Sprintf("Bearer %s", authToken), r.Header.Get("Authorization"))
require.Equal(t, http.MethodGet, r.Method)
_, err := w.Write([]byte(`{"data":[]}`))
require.NoError(t, err)
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()

id, err := getID(testServer.URL, authToken)
require.Contains(t, err.String(), "no Hana instances found in the response")
require.Empty(t, id)
})
t.Run("can't create new GET request", func(t *testing.T) {
id, err := getID(": ", authToken)
require.Contains(t, err.String(), "failed to create a get Hana instances request")
require.Empty(t, id)
})
t.Run("can't send GET request", func(t *testing.T) {
id, err := getID("https://localhost", authToken)
require.Contains(t, err.String(), "failed to get Hana instances")
require.Empty(t, id)
})
t.Run("response status not OK", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer testServer.Close()

id, err := getID(testServer.URL, authToken)
require.Contains(t, err.String(), "unexpected status code: 500")
require.Empty(t, id)
})
t.Run("failed to unmarshal response", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(`wrong format {`))
require.NoError(t, err)
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()

id, err := getID(testServer.URL, authToken)
require.Contains(t, err.String(), "failed to parse Hana instances from the response")
require.Empty(t, id)
})
}
45 changes: 45 additions & 0 deletions internal/btp/hana/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package hana

import (
"bytes"
"encoding/json"
"fmt"
"github.com/kyma-project/cli.v3/internal/clierror"
"net/http"
)

func MapInstance(baseURL, clusterID, hanaID, token string) clierror.Error {
return mapInstance(fmt.Sprintf("https://%s", baseURL), clusterID, hanaID, token)
}

func mapInstance(baseURL, clusterID, hanaID, token string) clierror.Error {
client := &http.Client{}

requestData := HanaMapping{
Platform: "kubernetes",
PrimaryID: clusterID,
}

requestString, err := json.Marshal(requestData)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to marshal mapping request"))
}

request, err := http.NewRequest("POST", fmt.Sprintf("%s/inventory/v2/serviceInstances/%s/instanceMappings", baseURL, hanaID), bytes.NewBuffer(requestString))
if err != nil {
return clierror.Wrap(err, clierror.New("failed to create mapping request"))
}
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

resp, err := client.Do(request)
if err != nil {
return clierror.Wrap(err, clierror.New("failed to post mapping request"))
}

// server sends status Created when mapping is created, and 200 if it already exists
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return clierror.Wrap(fmt.Errorf("status code: %d", resp.StatusCode), clierror.New("unexpected status code"))
}

return nil
}
44 changes: 44 additions & 0 deletions internal/btp/hana/map_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package hana

import (
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"testing"
)

func TestMapInstance(t *testing.T) {
t.Run("map hana instance", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/inventory/v2/serviceInstances/test-id/instanceMappings", r.URL.Path)
require.Equal(t, fmt.Sprintf("Bearer %s", "test-token"), r.Header.Get("Authorization"))
require.Equal(t, http.MethodPost, r.Method)

w.WriteHeader(http.StatusCreated)
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()

err := mapInstance(testServer.URL, "test-cluster-id", "test-id", "test-token")
require.Nil(t, err)
})
t.Run("failed to create mapping request", func(t *testing.T) {
err := mapInstance(": ", "test-cluster-id", "test-id", "test-token")
require.Contains(t, err.String(), "failed to create mapping request")
})
t.Run("failed to post mapping request", func(t *testing.T) {

err := mapInstance("https://localhost", "test-cluster-id", "test-id", "test-token")
require.Contains(t, err.String(), "failed to post mapping request")
})
t.Run("unexpected status code", func(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer testServer.Close()

err := mapInstance(testServer.URL, "test-cluster-id", "test-id", "test-token")
require.Contains(t, err.String(), "unexpected status code")
})
}
23 changes: 23 additions & 0 deletions internal/btp/hana/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package hana

import "github.com/kyma-project/cli.v3/internal/btp/auth"

type HanaInstance struct {
Data []HanaInstanceData `json:"data"`
}

type HanaInstanceData struct {
ID string `json:"id"`
Name string `json:"name"`
ServicePlanName string `json:"servicePlanName"`
}

type HanaAdminCredentials struct {
BaseURL string `json:"baseurl"`
UAA auth.UAA `json:"uaa"`
}

type HanaMapping struct {
Platform string `json:"platform"`
PrimaryID string `json:"primaryID"`
}
2 changes: 2 additions & 0 deletions internal/cmd/alpha/alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/access"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/add"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/hana"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/modules"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/oidc"
"github.com/kyma-project/cli.v3/internal/cmd/alpha/provision"
Expand All @@ -29,6 +30,7 @@ func NewAlphaCMD() (*cobra.Command, clierror.Error) {
return nil, err
}

cmd.AddCommand(hana.NewHanaCMD(kymaConfig))
cmd.AddCommand(provision.NewProvisionCMD())
cmd.AddCommand(referenceinstance.NewReferenceInstanceCMD(kymaConfig))
cmd.AddCommand(access.NewAccessCMD(kymaConfig))
Expand Down
19 changes: 19 additions & 0 deletions internal/cmd/alpha/hana/hana.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package hana

import (
"github.com/kyma-project/cli.v3/internal/cmdcommon"
"github.com/spf13/cobra"
)

func NewHanaCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
cmd := &cobra.Command{
Use: "hana",
Short: "Manage a Hana instance on the Kyma platform.",
Long: `Use this command to manage a Hana instance on the SAP Kyma platform.`,
DisableFlagsInUseLine: true,
}

cmd.AddCommand(NewMapHanaCMD(kymaConfig))

return cmd
}
90 changes: 90 additions & 0 deletions internal/cmd/alpha/hana/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package hana

import (
"context"
"github.com/kyma-project/cli.v3/internal/btp/auth"
"github.com/kyma-project/cli.v3/internal/btp/hana"
"github.com/kyma-project/cli.v3/internal/clierror"
"github.com/kyma-project/cli.v3/internal/cmdcommon"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type hanaMapConfig struct {
*cmdcommon.KymaConfig

hanaID string
credentialsPath string
}

func NewMapHanaCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
config := hanaMapConfig{
KymaConfig: kymaConfig,
}

cmd := &cobra.Command{
Use: "map",
Short: "Map the Hana instance to the Kyma cluster.",
Long: "Use this command to map the Hana instance to the Kyma cluster.",
Run: func(_ *cobra.Command, _ []string) {
clierror.Check(runHanaMap(&config))
},
}

cmd.Flags().StringVar(&config.credentialsPath, "credentials-path", "", "Path to the credentials json file.")
cmd.Flags().StringVar(&config.hanaID, "hana-id", "", "Hana instance ID.")
_ = cmd.MarkFlagRequired("credentials-path")

return cmd
}

func runHanaMap(config *hanaMapConfig) clierror.Error {
client, err := config.GetKubeClientWithClierr()
if err != nil {
return err
}

clusterID, err := getClusterID(config.Ctx, client.Static())
if err != nil {
return clierror.WrapE(err, clierror.New("while getting cluster ID"))
}

credentials, err := hana.ReadCredentialsFromFile(config.credentialsPath)
if err != nil {
return clierror.WrapE(err, clierror.New("while reading Hana credentials from file"))
}

token, err := auth.GetOAuthToken("client_credentials", credentials.UAA.URL, credentials.UAA.ClientID, credentials.UAA.ClientSecret)
if err != nil {
return clierror.WrapE(err, clierror.New("while getting OAuth token"))
}

// get Hana ID if not provided by the user
hanaID := config.hanaID
if hanaID == "" {
hanaID, err = hana.GetID(credentials.BaseURL, token.AccessToken)
if err != nil {
return clierror.WrapE(err, clierror.New("while getting hana ID"))

}
}
err = hana.MapInstance(credentials.BaseURL, clusterID, hanaID, token.AccessToken)
if err != nil {
return clierror.WrapE(err, clierror.New("while mapping Hana instance"))
}
return nil
}

func getClusterID(ctx context.Context, client kubernetes.Interface) (string, clierror.Error) {
secret, geterr := client.CoreV1().Secrets("kyma-system").Get(ctx, "sap-btp-manager", metav1.GetOptions{})
if geterr != nil {
return "", clierror.Wrap(geterr, clierror.New("failed to get secret kyma-system/sap-btp-manager"))
}

if secret.Data["cluster_id"] == nil {
return "", clierror.New("cluster_id not found in the secret kyma-system/sap-btp-manager")
}

return string(secret.Data["cluster_id"]), nil
}

0 comments on commit 336782b

Please sign in to comment.