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..bb2295b 100644 --- a/pkg/client/client_rest.go +++ b/pkg/client/client_rest.go @@ -4,14 +4,15 @@ import ( "bytes" "context" "crypto/tls" - "encoding/base64" "encoding/json" "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 +33,13 @@ var ( type RestClient struct { uri *url.URL httpClient *http.Client - cred ReqInjector - username string - password string + auth Credentials Token string + logger *slog.Logger } -type ReqInjector interface { - Inject(*http.Request) error +type Credentials interface { + Apply(*http.Request) error } type basicCredentials struct { @@ -47,24 +47,24 @@ 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 } type tokenCredentials string -func (c *tokenCredentials) Inject(r *http.Request) error { - r.Header.Add("Authorization", fmt.Sprintf("Bearer %v", 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 +98,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 +138,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 +185,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 +202,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 +232,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 +268,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 +300,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 +337,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 +423,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 +457,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 +470,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 +483,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 }