From 6c9d922f1a86a15f827b21aa16387249f262792a Mon Sep 17 00:00:00 2001 From: Amir Mofasser Date: Mon, 9 Mar 2026 13:01:59 +0100 Subject: [PATCH] feat(client): refactor client into an interface Refactored RestClient implementation into a new client_rest.go file. Introduced Client interface in client.go for improved abstraction and testability. Updated command code to use interface and type assertions where needed. --- cmd/inspect/inspect.go | 4 +- cmd/list/list.go | 12 +- cmd/login/login.go | 4 +- pkg/client/client.go | 366 +------------------------------- pkg/client/client_rest.go | 426 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 445 insertions(+), 367 deletions(-) create mode 100644 pkg/client/client_rest.go diff --git a/cmd/inspect/inspect.go b/cmd/inspect/inspect.go index caaa061..c3750c5 100644 --- a/cmd/inspect/inspect.go +++ b/cmd/inspect/inspect.go @@ -78,8 +78,8 @@ Examples: if _, ok := conf.AuthInfos[authName]; !ok { return errors.New("credentials missing! Please run 'tcli login' to authenticate") } - c.SetToken(conf.AuthInfos[authName].Token) - c.SetInsecure(insecureSkipVerify) + c.(*client.RestClient).SetToken(conf.AuthInfos[authName].Token) + c.(*client.RestClient).SetInsecure(insecureSkipVerify) // Check if there is a namespace set in the context that we can use so that we don't have to specify the --namespace flag if _, ok := conf.Contexts[contextName]; ok && len(tanzuNamespace) == 0 { diff --git a/cmd/list/list.go b/cmd/list/list.go index 88d7101..68370fd 100644 --- a/cmd/list/list.go +++ b/cmd/list/list.go @@ -90,8 +90,8 @@ Examples: if _, ok := conf.AuthInfos[authName]; !ok { return errors.New("credentials missing! Please run 'tcli login' to authenticate") } - c.SetToken(conf.AuthInfos[authName].Token) - c.SetInsecure(insecureSkipVerify) + c.(*client.RestClient).SetToken(conf.AuthInfos[authName].Token) + c.(*client.RestClient).SetInsecure(insecureSkipVerify) // Check if there is a namespace set in the context that we can use so that we don't have to specify the --namespace flag if _, ok := conf.Contexts[contextName]; ok && len(tanzuNamespace) == 0 { @@ -127,7 +127,7 @@ Examples: return c } -func listClusters(ctx context.Context, c *client.RestClient, ns string) error { +func listClusters(ctx context.Context, c client.Client, ns string) error { objs, err := c.Clusters(ctx, ns) if err != nil { return err @@ -140,7 +140,7 @@ func listClusters(ctx context.Context, c *client.RestClient, ns string) error { return nil } -func listReleases(ctx context.Context, c *client.RestClient) error { +func listReleases(ctx context.Context, c client.Client) error { objs, err := c.ReleasesTable(ctx) if err != nil { return err @@ -153,7 +153,7 @@ func listReleases(ctx context.Context, c *client.RestClient) error { return nil } -func listNamespaces(ctx context.Context, c *client.RestClient, username, password string) error { +func listNamespaces(ctx context.Context, c client.Client, username, password string) error { err := c.Login(ctx, username, password) if err != nil { return err @@ -168,7 +168,7 @@ func listNamespaces(ctx context.Context, c *client.RestClient, username, passwor return nil } -func listAddons(ctx context.Context, c *client.RestClient) error { +func listAddons(ctx context.Context, c client.Client) error { objs, err := c.AddonsTable(ctx) if err != nil { return err diff --git a/cmd/login/login.go b/cmd/login/login.go index cc5492a..9ee77e3 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -90,7 +90,7 @@ Examples: if err != nil { return err } - c.SetInsecure(insecureSkipVerify) + c.(*client.RestClient).SetInsecure(insecureSkipVerify) err = c.Login(ctx, tanzuUsername, tanzuPassword) if err != nil { return err @@ -108,7 +108,7 @@ Examples: authName := fmt.Sprintf("wcp:%s:%s", u.Host, tanzuUsername) auth := api.NewAuthInfo() - auth.Token = c.Token + auth.Token = c.(*client.RestClient).Token context := api.NewContext() context.Cluster = u.Host diff --git a/pkg/client/client.go b/pkg/client/client.go index f7307d4..b8ba01e 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,367 +1,19 @@ package client import ( - "bytes" "context" - "crypto/tls" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "sort" "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var ErrClusterNotFound = errors.New("cluster not found") - -type RestClient struct { - u *url.URL - c *http.Client - cred ReqInjector - username string - password string - Token string -} - -type ReqInjector interface { - Inject(*http.Request) error -} - -type basicCredentials struct { - username string - password string -} - -func (c *basicCredentials) Inject(r *http.Request) error { - r.Header.Add("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", c.username, c.password)))) - return nil -} - -type tokenCredentials string - -func (c *tokenCredentials) Inject(r *http.Request) error { - r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", c)) - return nil -} - -func TokenCredentials(token string) ReqInjector { - t := tokenCredentials(token) - return &t -} - -func BasicCredentials(username, password string) ReqInjector { - return &basicCredentials{ - username: username, - password: password, - } -} - -type Option func(*RestClient) - -type LoginResponse struct { - SessionID string `json:"session_id,omitempty"` -} - -type LoginClusterResponse struct { - LoginResponse - GuestClusterServer string `json:"guest_cluster_server,omitempty"` - GuestClusterCa string `json:"guest_cluster_ca,omitempty"` -} - -type Namespace struct { - Namespace string `json:"namespace,omitempty"` - MasterHost string `json:"master_host,omitempty"` - ConrolPlaneAPIServerPort string `json:"conrol_plane_api_server_port,omitempty"` - ControlPlaneDNSNames []string `json:"control_plane_dns_names,omitempty"` -} - -func WithClient(c *http.Client) Option { - return func(r *RestClient) { - r.c = c - } -} - -func WithInsecure(insecure bool) Option { - return func(rc *RestClient) { - rc.c.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = insecure - } -} - -func WithCredentials(creds ReqInjector) Option { - return func(rc *RestClient) { - rc.cred = creds - } -} - -func New(baseURL string) (*RestClient, error) { - u, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - return &RestClient{ - u: u, - c: http.DefaultClient, - }, nil -} - -func (r *RestClient) SetInsecure(t bool) *RestClient { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - return r -} - -func (r *RestClient) SetToken(t string) *RestClient { - r.Token = t - return r -} - -func (r *RestClient) Namespaces(ctx context.Context) ([]Namespace, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/wcp/workloads", r.u.String()), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", r.username, r.password)))}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - - var namespaces []Namespace - err = json.Unmarshal(body, &namespaces) - if err != nil { - return nil, err - } - - // Sort namespaces by name - sort.SliceStable(namespaces, func(i, j int) bool { - return namespaces[i].Namespace < namespaces[j].Namespace - }) - - return namespaces, nil -} - -func (r *RestClient) Clusters(ctx context.Context, ns string) (*v1.Table, error) { - if len(ns) == 0 { - ns = "default" - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters?limit=500", r.u.String(), ns), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, - "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - var clusterlist v1.Table - err = json.Unmarshal(body, &clusterlist) - if err != nil { - return nil, err - } - return &clusterlist, nil -} - -func (r *RestClient) ReleasesTable(ctx context.Context) (*v1.Table, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesreleases?limit=500", r.u.String()), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, - "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - var releases v1.Table - err = json.Unmarshal(body, &releases) - if err != nil { - return nil, err - } - return &releases, nil -} - -func (r *RestClient) AddonsTable(ctx context.Context) (*v1.Table, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesaddons?limit=500", r.u.String()), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, - "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - var addons v1.Table - err = json.Unmarshal(body, &addons) - if err != nil { - return nil, err - } - return &addons, nil -} - -func (r *RestClient) Releases(ctx context.Context) (*v1alpha2.TanzuKubernetesReleaseList, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesreleases?limit=500", r.u.String()), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - var releases v1alpha2.TanzuKubernetesReleaseList - err = json.Unmarshal(body, &releases) - if err != nil { - return nil, err - } - return &releases, nil -} - -func (r *RestClient) Cluster(ctx context.Context, ns, name string) (*v1alpha2.TanzuKubernetesCluster, error) { - if len(ns) == 0 { - ns = "default" - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters/%s", r.u.String(), ns, name), nil) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - var cluster v1alpha2.TanzuKubernetesCluster - err = json.Unmarshal(body, &cluster) - if err != nil { - return nil, err - } - return &cluster, nil -} - -func (r *RestClient) Login(ctx context.Context, u, p string) error { - r.username = u - r.password = p - req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/wcp/login", r.u.String()), nil) - if err != nil { - return err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", u, p)))}, - } - resp, err := r.c.Do(req) - if err != nil { - return err - } - body, err := handleResponse(resp) - if err != nil { - return err - } - - var login LoginResponse - err = json.Unmarshal(body, &login) - if err != nil { - return err - } - r.Token = login.SessionID - return nil -} - -func (r *RestClient) LoginCluster(ctx context.Context, cluster, namespace string) (*LoginClusterResponse, error) { - data := fmt.Sprintf("{\"guest_cluster_name\":\"%s\"}", cluster) - if len(namespace) > 0 { - data = fmt.Sprintf("{\"guest_cluster_name\":\"%s\", \"guest_cluster_namespace\":\"%s\"}", cluster, namespace) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/wcp/login", r.u.String()), bytes.NewBuffer([]byte(data))) - if err != nil { - return nil, err - } - req.Header = map[string][]string{ - "Content-Type": {"application/json"}, - "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", r.username, r.password)))}, - } - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - body, err := handleResponse(resp) - if err != nil { - return nil, err - } - - var login LoginClusterResponse - err = json.Unmarshal(body, &login) - if err != nil { - return nil, err - } - - // An 'guest_cluster_server' in response means a not-found error - if len(login.GuestClusterServer) == 0 { - return nil, ErrClusterNotFound - } - return &login, nil -} - -func handleResponse(resp *http.Response) ([]byte, error) { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - defer func() { - _ = resp.Body.Close() - }() - - statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300 - if !statusOK { - return nil, errors.New(string(body)) - } - return body, nil +type Client interface { + Namespaces(ctx context.Context) ([]Namespace, error) + ReleasesTable(ctx context.Context) (*v1.Table, error) + AddonsTable(ctx context.Context) (*v1.Table, error) + Releases(ctx context.Context) (*v1alpha2.TanzuKubernetesReleaseList, error) + Cluster(ctx context.Context, ns, name string) (*v1alpha2.TanzuKubernetesCluster, error) + Clusters(ctx context.Context, ns string) (*v1.Table, error) + Login(ctx context.Context, u, p string) error + LoginCluster(ctx context.Context, cluster, namespace string) (*LoginClusterResponse, error) } diff --git a/pkg/client/client_rest.go b/pkg/client/client_rest.go new file mode 100644 index 0000000..2898264 --- /dev/null +++ b/pkg/client/client_rest.go @@ -0,0 +1,426 @@ +package client + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sort" + + "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + ErrClusterNotFound = errors.New("cluster not found") + _ Client = &RestClient{} + + PathWCPWorkloads string = "/wcp/workloads" + PathWCPLogin string = "/wcp/login" + PathTanzuKubernetesClusters string = "/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters" + PathTanzuKubernetesCluster string = "/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters/%s" + PathTanzuKubernetesReleases string = "/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesreleases" + PathTanzuKubernetesAddons string = "/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesaddons" +) + +type RestClient struct { + uri *url.URL + httpClient *http.Client + cred ReqInjector + username string + password string + Token string +} + +type ReqInjector interface { + Inject(*http.Request) error +} + +type basicCredentials struct { + username string + password string +} + +func (c *basicCredentials) Inject(r *http.Request) error { + r.Header.Add("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", c.username, c.password)))) + return nil +} + +type tokenCredentials string + +func (c *tokenCredentials) Inject(r *http.Request) error { + r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", c)) + return nil +} + +func TokenCredentials(token string) ReqInjector { + t := tokenCredentials(token) + return &t +} + +func BasicCredentials(username, password string) ReqInjector { + return &basicCredentials{ + username: username, + password: password, + } +} + +type Option func(*RestClient) + +type LoginResponse struct { + SessionID string `json:"session_id,omitempty"` +} + +type LoginClusterResponse struct { + LoginResponse + GuestClusterServer string `json:"guest_cluster_server,omitempty"` + GuestClusterCa string `json:"guest_cluster_ca,omitempty"` +} + +type Namespace struct { + Namespace string `json:"namespace,omitempty"` + MasterHost string `json:"master_host,omitempty"` + ConrolPlaneAPIServerPort string `json:"conrol_plane_api_server_port,omitempty"` + ControlPlaneDNSNames []string `json:"control_plane_dns_names,omitempty"` +} + +func WithClient(c *http.Client) Option { + return func(r *RestClient) { + r.httpClient = c + } +} + +func WithInsecure(insecure bool) Option { + return func(rc *RestClient) { + rc.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = insecure + } +} + +func WithCredentials(creds ReqInjector) Option { + return func(rc *RestClient) { + rc.cred = creds + } +} + +func (r *RestClient) SetInsecure(t bool) *RestClient { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + return r +} + +func (r *RestClient) SetToken(t string) *RestClient { + r.Token = t + return r +} + +// getRequestURI builds an URI for the given path. It uses the base URI when RestClient was created with [New] +func (r *RestClient) getRequestURI(path string) (*url.URL, error) { + newPath, err := url.JoinPath(r.uri.Path, path) + if err != nil { + return nil, err + } + newURL := *r.uri + newURL.Path = newPath + return &newURL, nil +} + +func (r *RestClient) Namespaces(ctx context.Context) ([]Namespace, error) { + u, err := r.getRequestURI(PathWCPWorkloads) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", r.username, r.password)))}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + + var namespaces []Namespace + err = json.Unmarshal(body, &namespaces) + if err != nil { + return nil, err + } + + // Sort namespaces by name + sort.SliceStable(namespaces, func(i, j int) bool { + return namespaces[i].Namespace < namespaces[j].Namespace + }) + + return namespaces, nil +} + +func (r *RestClient) Clusters(ctx context.Context, ns string) (*v1.Table, error) { + if len(ns) == 0 { + ns = "default" + } + + u, err := r.getRequestURI(PathTanzuKubernetesClusters) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(u.String(), ns), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, + "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + var clusterlist v1.Table + err = json.Unmarshal(body, &clusterlist) + if err != nil { + return nil, err + } + return &clusterlist, nil +} + +func (r *RestClient) ReleasesTable(ctx context.Context) (*v1.Table, error) { + u, err := r.getRequestURI(PathTanzuKubernetesReleases) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, + "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + var releases v1.Table + err = json.Unmarshal(body, &releases) + if err != nil { + return nil, err + } + return &releases, nil +} + +func (r *RestClient) AddonsTable(ctx context.Context) (*v1.Table, error) { + u, err := r.getRequestURI(PathTanzuKubernetesAddons) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Accept": {"application/json;as=Table;g=meta.k8s.io;v=v1"}, + "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + var addons v1.Table + err = json.Unmarshal(body, &addons) + if err != nil { + return nil, err + } + return &addons, nil +} + +func (r *RestClient) Releases(ctx context.Context) (*v1alpha2.TanzuKubernetesReleaseList, error) { + u, err := r.getRequestURI(PathTanzuKubernetesReleases) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + var releases v1alpha2.TanzuKubernetesReleaseList + err = json.Unmarshal(body, &releases) + if err != nil { + return nil, err + } + return &releases, nil +} + +func (r *RestClient) Cluster(ctx context.Context, ns, name string) (*v1alpha2.TanzuKubernetesCluster, error) { + if len(ns) == 0 { + ns = "default" + } + u, err := r.getRequestURI(PathTanzuKubernetesCluster) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(u.String(), ns, name), nil) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", r.Token)}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + var cluster v1alpha2.TanzuKubernetesCluster + err = json.Unmarshal(body, &cluster) + if err != nil { + return nil, err + } + return &cluster, nil +} + +func (r *RestClient) Login(ctx context.Context, u, p string) error { + r.username = u + r.password = p + + uri, err := r.getRequestURI(PathWCPLogin) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil) + if err != nil { + return err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", u, p)))}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return err + } + body, err := handleResponse(resp) + if err != nil { + return err + } + + var login LoginResponse + err = json.Unmarshal(body, &login) + if err != nil { + return err + } + r.Token = login.SessionID + return nil +} + +func (r *RestClient) LoginCluster(ctx context.Context, cluster, namespace string) (*LoginClusterResponse, error) { + data := fmt.Sprintf("{\"guest_cluster_name\":\"%s\"}", cluster) + if len(namespace) > 0 { + data = fmt.Sprintf("{\"guest_cluster_name\":\"%s\", \"guest_cluster_namespace\":\"%s\"}", cluster, namespace) + } + uri, err := r.getRequestURI(PathWCPLogin) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), bytes.NewBuffer([]byte(data))) + if err != nil { + return nil, err + } + req.Header = map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", r.username, r.password)))}, + } + resp, err := r.httpClient.Do(req) + if err != nil { + return nil, err + } + body, err := handleResponse(resp) + if err != nil { + return nil, err + } + + var login LoginClusterResponse + err = json.Unmarshal(body, &login) + if err != nil { + return nil, err + } + + // An 'guest_cluster_server' in response means a not-found error + if len(login.GuestClusterServer) == 0 { + return nil, ErrClusterNotFound + } + return &login, nil +} + +func handleResponse(resp *http.Response) ([]byte, error) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + defer func() { + _ = resp.Body.Close() + }() + + statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !statusOK { + return nil, errors.New(string(body)) + } + return body, nil +} + +func New(baseURI string) (Client, error) { + u, err := url.ParseRequestURI(baseURI) + if err != nil { + return nil, err + } + return &RestClient{ + uri: u, + httpClient: http.DefaultClient, + }, nil +}