From 2992bf69de033c91af4c8272fec8be5d32c9e299 Mon Sep 17 00:00:00 2001 From: Amir Mofasser Date: Tue, 10 Mar 2026 07:38:06 +0100 Subject: [PATCH 1/3] refactor(client): add logger and credentials options Added logger and credentials options to RestClient for improved logging and flexible authentication. Refactored request handling to use new interfaces and options. Updated client interface and call sites for compatibility. --- cmd/inspect/inspect.go | 18 +-- cmd/list/list.go | 26 ++-- cmd/login/login.go | 12 +- pkg/client/client.go | 2 +- pkg/client/client_rest.go | 260 +++++++++++++++++++++++++------------- 5 files changed, 209 insertions(+), 109 deletions(-) diff --git a/cmd/inspect/inspect.go b/cmd/inspect/inspect.go index c3750c5..434b298 100644 --- a/cmd/inspect/inspect.go +++ b/cmd/inspect/inspect.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net/url" "os" @@ -56,12 +57,6 @@ Examples: return err } - // Create rest client - c, err := client.New(tanzuServer) - if err != nil { - return err - } - // Find credentials from kubeconfig context contextName := u.Host if _, ok := conf.Contexts[contextName]; !ok { @@ -78,14 +73,21 @@ Examples: if _, ok := conf.AuthInfos[authName]; !ok { return errors.New("credentials missing! Please run 'tcli login' to authenticate") } - 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 { tanzuNamespace = conf.Contexts[contextName].Namespace } + token := conf.AuthInfos[authName].Token + + // Create rest client + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + c, err := client.New(tanzuServer, client.WithLogger(logger), client.WithCredentials(client.TokenCredentials(token)), client.WithInsecure(insecureSkipVerify)) + if err != nil { + return err + } + cluster, err := c.Cluster(ctx, tanzuNamespace, tanzuCluster) if err != nil { return err diff --git a/cmd/list/list.go b/cmd/list/list.go index 68370fd..e61e7ec 100644 --- a/cmd/list/list.go +++ b/cmd/list/list.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net/url" "os" "strings" @@ -68,12 +69,6 @@ Examples: return err } - // Create rest client - c, err := client.New(tanzuServer) - if err != nil { - return err - } - // Find credentials from kubeconfig context contextName := u.Host if _, ok := conf.Contexts[contextName]; !ok { @@ -90,10 +85,16 @@ Examples: if _, ok := conf.AuthInfos[authName]; !ok { return errors.New("credentials missing! Please run 'tcli login' to authenticate") } - 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 + token := conf.AuthInfos[authName].Token + + // Create rest client + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) + c, err := client.New(tanzuServer, client.WithLogger(logger), client.WithCredentials(client.TokenCredentials(token)), client.WithInsecure(insecureSkipVerify)) + if err != nil { + return err + } + if _, ok := conf.Contexts[contextName]; ok && len(tanzuNamespace) == 0 { tanzuNamespace = conf.Contexts[contextName].Namespace } @@ -111,7 +112,7 @@ Examples: tanzuPassword = string(bytePassword) fmt.Printf("\n") } - return listNamespaces(ctx, c, tanzuUsername, tanzuPassword) + return listNamespaces(ctx, tanzuServer, tanzuUsername, tanzuPassword, insecureSkipVerify) case "clusters", "clu", "tkc": return listClusters(ctx, c, tanzuNamespace) case "releases", "rel", "tkr": @@ -153,11 +154,12 @@ func listReleases(ctx context.Context, c client.Client) error { return nil } -func listNamespaces(ctx context.Context, c client.Client, username, password string) error { - err := c.Login(ctx, username, password) +func listNamespaces(ctx context.Context, server, username, password string, insecure bool) error { + c, err := client.New(server, client.WithCredentials(client.BasicCredentials(username, password)), client.WithInsecure(insecure)) if err != nil { return err } + nsList, err := c.Namespaces(ctx) if err != nil { return err diff --git a/cmd/login/login.go b/cmd/login/login.go index 9ee77e3..99c10da 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -5,10 +5,13 @@ import ( "encoding/base64" "errors" "fmt" + "log/slog" "net/url" + "os" "syscall" "github.com/middlewaregruppen/tcli/pkg/client" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/term" @@ -86,16 +89,19 @@ Examples: return err } - c, err := client.New(tanzuServer) + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + c, err := client.New(tanzuServer, client.WithLogger(logger), client.WithCredentials(client.BasicCredentials(tanzuUsername, tanzuPassword))) if err != nil { return err } c.(*client.RestClient).SetInsecure(insecureSkipVerify) - err = c.Login(ctx, tanzuUsername, tanzuPassword) + sess, err := c.Login(ctx, tanzuUsername, tanzuPassword) if err != nil { return err } + logrus.Debug("successfully logged in to cluster") + ns, err := c.Namespaces(ctx) if err != nil { return err @@ -108,7 +114,7 @@ Examples: authName := fmt.Sprintf("wcp:%s:%s", u.Host, tanzuUsername) auth := api.NewAuthInfo() - auth.Token = c.(*client.RestClient).Token + auth.Token = sess.SessionID context := api.NewContext() context.Cluster = u.Host diff --git a/pkg/client/client.go b/pkg/client/client.go index b8ba01e..e1d3daa 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,6 +14,6 @@ type Client interface { 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 + Login(ctx context.Context, u, p string) (*LoginResponse, error) LoginCluster(ctx context.Context, cluster, namespace string) (*LoginClusterResponse, error) } diff --git a/pkg/client/client_rest.go b/pkg/client/client_rest.go index 2898264..a63ed82 100644 --- a/pkg/client/client_rest.go +++ b/pkg/client/client_rest.go @@ -9,9 +9,11 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "net/url" "sort" + "time" "github.com/vmware-tanzu/tanzu-framework/apis/run/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,14 +34,17 @@ var ( type RestClient struct { uri *url.URL httpClient *http.Client - cred ReqInjector + auth Credentials username string password string Token string + opts []Option + logger *slog.Logger } -type ReqInjector interface { - Inject(*http.Request) error +type Credentials interface { + Apply(*http.Request) error + Raw() []byte } type basicCredentials struct { @@ -47,24 +52,33 @@ type basicCredentials struct { 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)))) +func (c *basicCredentials) Apply(r *http.Request) error { + r.SetBasicAuth(c.username, c.password) return nil } +func (c *basicCredentials) Raw() []byte { + return []byte(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", c.username, c.password))) +} + type tokenCredentials string -func (c *tokenCredentials) Inject(r *http.Request) error { - r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", c)) +// Raw implements [Credentials]. +func (c *tokenCredentials) Raw() []byte { + return []byte(*c) +} + +func (c *tokenCredentials) Apply(r *http.Request) error { + r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", *c)) return nil } -func TokenCredentials(token string) ReqInjector { +func TokenCredentials(token string) Credentials { t := tokenCredentials(token) return &t } -func BasicCredentials(username, password string) ReqInjector { +func BasicCredentials(username, password string) Credentials { return &basicCredentials{ username: username, password: password, @@ -98,13 +112,33 @@ func WithClient(c *http.Client) Option { func WithInsecure(insecure bool) Option { return func(rc *RestClient) { - rc.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = insecure + if rc.httpClient == nil { + rc.httpClient = &http.Client{} + } + + baseTransport := rc.httpClient.Transport + if baseTransport == nil { + baseTransport = http.DefaultTransport + } + + transport := baseTransport.(*http.Transport).Clone() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = insecure + rc.httpClient.Transport = transport } } -func WithCredentials(creds ReqInjector) Option { +func WithCredentials(creds Credentials) Option { return func(rc *RestClient) { - rc.cred = creds + rc.auth = creds + } +} + +func WithLogger(l *slog.Logger) Option { + return func(rc *RestClient) { + rc.logger = l } } @@ -118,6 +152,45 @@ func (r *RestClient) SetToken(t string) *RestClient { return r } +// DoRequest applies options and then performs the http request +func (r *RestClient) DoRequest(req *http.Request) (*http.Response, error) { + if r.auth != nil { + if err := r.auth.Apply(req); err != nil { + r.logger.Debug("error applying credentials to request", + "method", req.Method, + "url", req.URL.String(), + "auth_method", &r.auth, + "error", err, + ) + return nil, err + } + } + + req.Header.Add("Content-Type", "application/json") + start := time.Now() + + res, err := r.httpClient.Do(req) + if err != nil { + r.logger.Debug("http request failed", + "method", req.Method, + "url", req.URL.String(), + "duration", time.Since(start), + "error", err, + ) + return nil, err + } + + r.logger.Debug("http request executed", + "method", req.Method, + "url", req.URL.String(), + "duration", time.Since(start), + "error", err, + "res_content_type", res.Header.Values("Content-Type"), + "req_content_type", req.Header.Values("Content-Type"), + ) + return res, nil +} + // 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) @@ -126,6 +199,10 @@ func (r *RestClient) getRequestURI(path string) (*url.URL, error) { } newURL := *r.uri newURL.Path = newPath + r.logger.Debug("built request uri", + "path", path, + "uri", newURL.String(), + ) return &newURL, nil } @@ -139,15 +216,13 @@ func (r *RestClient) Namespaces(ctx context.Context) ([]Namespace, error) { 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) + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.handleResponse(resp) if err != nil { return nil, err } @@ -171,33 +246,34 @@ func (r *RestClient) Clusters(ctx context.Context, ns string) (*v1.Table, error) ns = "default" } - u, err := r.getRequestURI(PathTanzuKubernetesClusters) + u, err := r.getRequestURI(fmt.Sprintf(PathTanzuKubernetesClusters, ns)) if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(u.String(), ns), nil) + 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) + + req.Header.Add("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1") + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.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 } @@ -206,28 +282,30 @@ func (r *RestClient) ReleasesTable(ctx context.Context) (*v1.Table, error) { 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) + + req.Header.Add("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1") + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.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 } @@ -236,28 +314,30 @@ func (r *RestClient) AddonsTable(ctx context.Context) (*v1.Table, error) { 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) + + req.Header.Add("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1") + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.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 } @@ -271,91 +351,85 @@ func (r *RestClient) Releases(ctx context.Context) (*v1alpha2.TanzuKubernetesRel 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) + + req.Header.Add("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1") + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.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) + u, err := r.getRequestURI(fmt.Sprintf(PathTanzuKubernetesCluster, ns, name)) if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(u.String(), ns, name), nil) + + 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) + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.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 - +func (r *RestClient) Login(ctx context.Context, u, p string) (*LoginResponse, error) { uri, err := r.getRequestURI(PathWCPLogin) if err != nil { - return err + return nil, 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)))}, + return nil, err } - resp, err := r.httpClient.Do(req) + + resp, err := r.DoRequest(req) if err != nil { - return err + return nil, err } - body, err := handleResponse(resp) + body, err := r.handleResponse(resp) if err != nil { - return err + return nil, err } var login LoginResponse err = json.Unmarshal(body, &login) if err != nil { - return err + return nil, err } - r.Token = login.SessionID - return nil + + return &login, nil } func (r *RestClient) LoginCluster(ctx context.Context, cluster, namespace string) (*LoginClusterResponse, error) { @@ -363,23 +437,23 @@ func (r *RestClient) LoginCluster(ctx context.Context, cluster, namespace string 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) + + resp, err := r.DoRequest(req) if err != nil { return nil, err } - body, err := handleResponse(resp) + + body, err := r.handleResponse(resp) if err != nil { return nil, err } @@ -397,9 +471,12 @@ func (r *RestClient) LoginCluster(ctx context.Context, cluster, namespace string return &login, nil } -func handleResponse(resp *http.Response) ([]byte, error) { +func (r *RestClient) handleResponse(resp *http.Response) ([]byte, error) { body, err := io.ReadAll(resp.Body) if err != nil { + r.logger.Debug("error reading response body", + "error", err, + ) return nil, err } @@ -407,6 +484,12 @@ func handleResponse(resp *http.Response) ([]byte, error) { _ = resp.Body.Close() }() + r.logger.Debug("read http response body", + "status_code", resp.StatusCode, + "length", len(body), + "content_type", resp.Header.Values("Content-Type"), + ) + statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300 if !statusOK { return nil, errors.New(string(body)) @@ -414,13 +497,20 @@ func handleResponse(resp *http.Response) ([]byte, error) { return body, nil } -func New(baseURI string) (Client, error) { +func New(baseURI string, opts ...Option) (Client, error) { u, err := url.ParseRequestURI(baseURI) if err != nil { return nil, err } - return &RestClient{ + c := &RestClient{ uri: u, httpClient: http.DefaultClient, - }, nil + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + + for _, opt := range opts { + opt(c) + } + + return c, nil } From 1b1f65a227ed24006a789a2bca66ef14589912e1 Mon Sep 17 00:00:00 2001 From: Amir Mofasser Date: Tue, 10 Mar 2026 08:35:57 +0100 Subject: [PATCH 2/3] refactor(client): remove unused fields from RestClient --- pkg/client/client_rest.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/client/client_rest.go b/pkg/client/client_rest.go index a63ed82..69d9599 100644 --- a/pkg/client/client_rest.go +++ b/pkg/client/client_rest.go @@ -35,10 +35,7 @@ type RestClient struct { uri *url.URL httpClient *http.Client auth Credentials - username string - password string Token string - opts []Option logger *slog.Logger } From 4ae5e86b82cd173e114f94b7b19d2bef413d06b6 Mon Sep 17 00:00:00 2001 From: Amir Mofasser Date: Tue, 10 Mar 2026 08:37:01 +0100 Subject: [PATCH 3/3] refactor(client): remove Credentials.Raw method --- pkg/client/client_rest.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pkg/client/client_rest.go b/pkg/client/client_rest.go index 69d9599..bb2295b 100644 --- a/pkg/client/client_rest.go +++ b/pkg/client/client_rest.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/tls" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -41,7 +40,6 @@ type RestClient struct { type Credentials interface { Apply(*http.Request) error - Raw() []byte } type basicCredentials struct { @@ -54,17 +52,8 @@ func (c *basicCredentials) Apply(r *http.Request) error { return nil } -func (c *basicCredentials) Raw() []byte { - return []byte(base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", c.username, c.password))) -} - type tokenCredentials string -// Raw implements [Credentials]. -func (c *tokenCredentials) Raw() []byte { - return []byte(*c) -} - func (c *tokenCredentials) Apply(r *http.Request) error { r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", *c)) return nil