diff --git a/cmd/inspect/inspect.go b/cmd/inspect/inspect.go index 844e4f2..caaa061 100644 --- a/cmd/inspect/inspect.go +++ b/cmd/inspect/inspect.go @@ -2,6 +2,7 @@ package inspect import ( "bytes" + "context" "errors" "fmt" "net/url" @@ -14,9 +15,7 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -var ( - tanzuNamespace string -) +var tanzuNamespace string func NewCmdInspect() *cobra.Command { c := &cobra.Command{ @@ -37,6 +36,8 @@ Examples: return nil }, RunE: func(cmd *cobra.Command, args []string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) + defer cancel() tanzuCluster := args[0] tanzuServer := viper.GetString("server") @@ -85,7 +86,7 @@ Examples: tanzuNamespace = conf.Contexts[contextName].Namespace } - cluster, err := c.Cluster(tanzuNamespace, tanzuCluster) + 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 f8eda4b..88d7101 100644 --- a/cmd/list/list.go +++ b/cmd/list/list.go @@ -1,6 +1,7 @@ package list import ( + "context" "errors" "fmt" "net/url" @@ -16,9 +17,7 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -var ( - tanzuNamespace string -) +var tanzuNamespace string func NewCmdList() *cobra.Command { c := &cobra.Command{ @@ -49,6 +48,8 @@ Examples: return nil }, RunE: func(cmd *cobra.Command, args []string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) + defer cancel() tanzuServer := viper.GetString("server") tanzuUsername := viper.GetString("username") @@ -110,13 +111,13 @@ Examples: tanzuPassword = string(bytePassword) fmt.Printf("\n") } - return listNamespaces(c, tanzuUsername, tanzuPassword) + return listNamespaces(ctx, c, tanzuUsername, tanzuPassword) case "clusters", "clu", "tkc": - return listClusters(c, tanzuNamespace) + return listClusters(ctx, c, tanzuNamespace) case "releases", "rel", "tkr": - return listReleases(c) + return listReleases(ctx, c) case "addons", "tka": - return listAddons(c) + return listAddons(ctx, c) default: return fmt.Errorf("%s is not a valid resource", a) } @@ -126,8 +127,8 @@ Examples: return c } -func listClusters(c *client.RestClient, ns string) error { - objs, err := c.Clusters(ns) +func listClusters(ctx context.Context, c *client.RestClient, ns string) error { + objs, err := c.Clusters(ctx, ns) if err != nil { return err } @@ -139,8 +140,8 @@ func listClusters(c *client.RestClient, ns string) error { return nil } -func listReleases(c *client.RestClient) error { - objs, err := c.ReleasesTable() +func listReleases(ctx context.Context, c *client.RestClient) error { + objs, err := c.ReleasesTable(ctx) if err != nil { return err } @@ -152,12 +153,12 @@ func listReleases(c *client.RestClient) error { return nil } -func listNamespaces(c *client.RestClient, username, password string) error { - err := c.Login(username, password) +func listNamespaces(ctx context.Context, c *client.RestClient, username, password string) error { + err := c.Login(ctx, username, password) if err != nil { return err } - nsList, err := c.Namespaces() + nsList, err := c.Namespaces(ctx) if err != nil { return err } @@ -167,8 +168,8 @@ func listNamespaces(c *client.RestClient, username, password string) error { return nil } -func listAddons(c *client.RestClient) error { - objs, err := c.AddonsTable() +func listAddons(ctx context.Context, c *client.RestClient) error { + objs, err := c.AddonsTable(ctx) if err != nil { return err } diff --git a/cmd/login/login.go b/cmd/login/login.go index f374c02..cc5492a 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -1,7 +1,9 @@ package login import ( + "context" "encoding/base64" + "errors" "fmt" "net/url" "syscall" @@ -23,7 +25,7 @@ var ( func NewCmdLogin() *cobra.Command { c := &cobra.Command{ Use: "login CLUSTER", - //Args: cobra.MaximumNArgs(1), + // Args: cobra.MaximumNArgs(1), Args: cobra.MinimumNArgs(0), Short: "Authenticate user with Tanzu namespaces and clusters", Long: `Authenticate user with Tanzu namespaces and clusters @@ -52,6 +54,7 @@ Examples: if err := viper.BindPFlags(cmd.Flags()); err != nil { return err } + // Read from stdin if password isn't set anywhere if len(viper.GetString("password")) == 0 { fmt.Printf("Password:") @@ -68,6 +71,8 @@ Examples: return nil }, RunE: func(cmd *cobra.Command, args []string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) + defer cancel() tanzuServer := viper.GetString("server") tanzuUsername := viper.GetString("username") @@ -86,12 +91,12 @@ Examples: return err } c.SetInsecure(insecureSkipVerify) - err = c.Login(tanzuUsername, tanzuPassword) + err = c.Login(ctx, tanzuUsername, tanzuPassword) if err != nil { return err } - ns, err := c.Namespaces() + ns, err := c.Namespaces(ctx) if err != nil { return err } @@ -138,8 +143,12 @@ Examples: // Range over args and perform login on each of them for _, arg := range args { tanzuCluster := arg - res, err := c.LoginCluster(tanzuCluster, tanzuNamespace) + res, err := c.LoginCluster(ctx, tanzuCluster, tanzuNamespace) if err != nil { + if errors.Is(err, client.ErrClusterNotFound) { + fmt.Printf("Cluster %s does not exist", tanzuCluster) + continue + } return err } caCertData, err := base64.StdEncoding.DecodeString(res.GuestClusterCa) @@ -178,6 +187,7 @@ Examples: return err } + fmt.Printf("Successfully logged into cluster %s", tanzuCluster) } return nil diff --git a/cmd/root.go b/cmd/root.go index 63a4c01..60269af 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/middlewaregruppen/tcli/cmd/inspect" "github.com/middlewaregruppen/tcli/cmd/list" @@ -26,6 +27,7 @@ var ( insecureSkipVerify bool verbosity string kubeconfig string + timeout time.Duration ) func init() { @@ -65,7 +67,7 @@ func NewDefaultCommand() *cobra.Command { // Check if kubeconfig exists, create if it doesn't if _, err := os.Stat(kubeconfig); errors.Is(err, os.ErrNotExist) { - _, err = os.OpenFile(kubeconfig, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + _, err = os.OpenFile(kubeconfig, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666) if err != nil { return err } @@ -84,6 +86,7 @@ func NewDefaultCommand() *cobra.Command { logrus.Fatal(err) } // Setup flags + c.PersistentFlags().DurationVar(&timeout, "timeout", 30*time.Second, "How long to wait for an operation before giving up") c.PersistentFlags().StringVarP(&verbosity, "verbosity", "v", "info", "number for the log level verbosity (debug, info, warn, error, fatal, panic)") c.PersistentFlags().StringVarP(&tanzuServer, "server", "s", "", "Address of the server to authenticate against.") c.PersistentFlags().StringVarP(&tanzuUsername, "username", "u", "", "Username to authenticate.") diff --git a/pkg/client/client.go b/pkg/client/client.go index 78abbcd..f7307d4 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -2,6 +2,7 @@ package client import ( "bytes" + "context" "crypto/tls" "encoding/base64" "encoding/json" @@ -16,17 +17,56 @@ import ( 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"` @@ -40,8 +80,26 @@ type Namespace struct { ControlPlaneDNSNames []string `json:"control_plane_dns_names,omitempty"` } -func New(baseUrl string) (*RestClient, error) { - u, err := url.Parse(baseUrl) +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 } @@ -61,14 +119,14 @@ func (r *RestClient) SetToken(t string) *RestClient { return r } -func (r *RestClient) Namespaces() ([]Namespace, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/wcp/workloads", r.u.String()), nil) +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([]byte(fmt.Sprintf("%s:%s", r.username, r.password))))}, + "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 { @@ -93,11 +151,11 @@ func (r *RestClient) Namespaces() ([]Namespace, error) { return namespaces, nil } -func (r *RestClient) Clusters(ns string) (*v1.Table, error) { +func (r *RestClient) Clusters(ctx context.Context, ns string) (*v1.Table, error) { if len(ns) == 0 { ns = "default" } - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters?limit=500", r.u.String(), ns), nil) + 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 } @@ -122,7 +180,7 @@ func (r *RestClient) Clusters(ns string) (*v1.Table, error) { return &clusterlist, nil } -func (r *RestClient) ReleasesTable() (*v1.Table, error) { +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 @@ -148,8 +206,8 @@ func (r *RestClient) ReleasesTable() (*v1.Table, error) { return &releases, nil } -func (r *RestClient) AddonsTable() (*v1.Table, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesaddons?limit=500", r.u.String()), 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 } @@ -174,8 +232,8 @@ func (r *RestClient) AddonsTable() (*v1.Table, error) { return &addons, nil } -func (r *RestClient) Releases() (*v1alpha2.TanzuKubernetesReleaseList, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/tanzukubernetesreleases?limit=500", r.u.String()), 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 } @@ -199,11 +257,11 @@ func (r *RestClient) Releases() (*v1alpha2.TanzuKubernetesReleaseList, error) { return &releases, nil } -func (r *RestClient) Cluster(ns, name string) (*v1alpha2.TanzuKubernetesCluster, error) { +func (r *RestClient) Cluster(ctx context.Context, ns, name string) (*v1alpha2.TanzuKubernetesCluster, error) { if len(ns) == 0 { ns = "default" } - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s:6443/apis/run.tanzu.vmware.com/v1alpha2/namespaces/%s/tanzukubernetesclusters/%s", r.u.String(), ns, name), nil) + 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 } @@ -227,17 +285,16 @@ func (r *RestClient) Cluster(ns, name string) (*v1alpha2.TanzuKubernetesCluster, return &cluster, nil } -func (r *RestClient) Login(u, p string) error { - +func (r *RestClient) Login(ctx context.Context, u, p string) error { r.username = u r.password = p - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/wcp/login", r.u.String()), nil) + 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([]byte(fmt.Sprintf("%s:%s", u, p))))}, + "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", u, p)))}, } resp, err := r.c.Do(req) if err != nil { @@ -257,18 +314,18 @@ func (r *RestClient) Login(u, p string) error { return nil } -func (r *RestClient) LoginCluster(cluster, namespace string) (*LoginClusterResponse, error) { +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.NewRequest(http.MethodPost, fmt.Sprintf("%s/wcp/login", r.u.String()), bytes.NewBuffer([]byte(data))) + 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([]byte(fmt.Sprintf("%s:%s", r.username, r.password))))}, + "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 { @@ -284,6 +341,11 @@ func (r *RestClient) LoginCluster(cluster, namespace string) (*LoginClusterRespo 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 } @@ -292,7 +354,11 @@ func handleResponse(resp *http.Response) ([]byte, error) { if err != nil { return nil, err } - defer resp.Body.Close() + + defer func() { + _ = resp.Body.Close() + }() + statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300 if !statusOK { return nil, errors.New(string(body))