From 10a036ece14db25ce9ff9706bc9f78df32641143 Mon Sep 17 00:00:00 2001 From: Sayan Biswas Date: Fri, 15 Mar 2024 18:35:35 +0530 Subject: [PATCH] Add exec provider support Add support for fetching token from external provider configured through kubeconfig's ExecConfig api. --- README.md | 3 +- internal/cmd/config/results/results.go | 2 +- internal/cmd/version/version.go | 2 +- internal/results/client/client.go | 33 ++--- internal/results/client/grpc.go | 33 +++-- internal/results/client/rest.go | 35 ++--- internal/results/config/config.go | 173 ++++++++++--------------- internal/results/config/extension.go | 9 +- internal/results/config/host.go | 60 +++++++++ 9 files changed, 178 insertions(+), 172 deletions(-) create mode 100644 internal/results/config/host.go diff --git a/README.md b/README.md index 4d025a5b..67196000 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ kubectl krew install test/tekton | Name | Group | Default | Description | |--------------------------|--------|---------|----------------------------------------| -| host | client | | Host address for the client to connect | | client-type | client | REST | Type of client can be GRPC or REST | +| host | client | | Host address for the client to connect | +| api-path | client | | API path to add to request | | insecure-skip-tls-verify | client | false | Skip host name verification | | timeout | client | 1m | Client context timeout | | certificate-authority | tls | | CA file path to use | diff --git a/internal/cmd/config/results/results.go b/internal/cmd/config/results/results.go index af2b5d00..3d65c55f 100644 --- a/internal/cmd/config/results/results.go +++ b/internal/cmd/config/results/results.go @@ -89,7 +89,7 @@ func (o *Options) Run(_ *cobra.Command, args []string) (err error) { switch { case o.View: - return o.PrinterFunc(o.Config.RawConfig(), o.IOStreams.Out) + return o.PrinterFunc(o.Config.GetObject(), o.IOStreams.Out) case o.Reset: return o.Config.Reset() case len(args) == 0: diff --git a/internal/cmd/version/version.go b/internal/cmd/version/version.go index d6ca0e0d..635a9ff7 100644 --- a/internal/cmd/version/version.go +++ b/internal/cmd/version/version.go @@ -10,7 +10,7 @@ import ( ) // TODO: remove this hard coding. -const clientVersion = "v0.1.0" +const clientVersion = "v0.1.1" const serverVersion = "v0.9.0" var ( diff --git a/internal/results/client/client.go b/internal/results/client/client.go index 6d0d4274..f8f6d016 100644 --- a/internal/results/client/client.go +++ b/internal/results/client/client.go @@ -6,6 +6,7 @@ import ( resultsv1alpha2 "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" "google.golang.org/grpc/status" "k8s.io/client-go/transport" + "net/url" "time" ) @@ -20,34 +21,20 @@ type Client interface { } type Config struct { - ClientType string - Host string - ImpersonationConfig *transport.ImpersonationConfig - Timeout time.Duration - TLSConfig *transport.TLSConfig - Token string + ClientType string + URL *url.URL + Timeout time.Duration + Transport *transport.Config } -func NewClient(c *Config) (Client, error) { - c.SetDefault() - - switch c.ClientType { +func NewClient(config *Config) (Client, error) { + switch config.ClientType { case GRPC: - return NewGRPCClient(c) + return NewGRPCClient(config) case REST: - return NewRESTClient(c) + return NewRESTClient(config) default: - return nil, errors.New("invalid client type") - } -} - -func (c *Config) SetDefault() { - if c.ClientType == "" { - c.ClientType = REST - } - - if c.Timeout == 0 { - c.Timeout = time.Minute + return NewRESTClient(config) } } diff --git a/internal/results/client/grpc.go b/internal/results/client/grpc.go index c34cc77c..3878fe6d 100644 --- a/internal/results/client/grpc.go +++ b/internal/results/client/grpc.go @@ -26,24 +26,23 @@ func NewGRPCClient(c *Config) (Client, error) { ctx, cancel := context.WithTimeout(context.Background(), c.Timeout) defer cancel() - u, err := url.Parse(c.Host) - if err != nil { - return nil, err + if c.Timeout == 0 { + ctx = context.Background() } - if u.Port() == "" { - switch u.Scheme { + if c.URL.Port() == "" { + switch c.URL.Scheme { case "https": - u.Host = u.Host + ":443" + c.URL.Host = c.URL.Host + ":443" case "http": - u.Host = u.Host + ":80" + c.URL.Host = c.URL.Host + ":80" default: return nil, errors.New("port or scheme missing in host") } } tc := insecure.NewCredentials() - if c.TLSConfig != nil && u.Scheme == "https" { + if c.URL.Scheme == "https" { tls, err := c.ClientTLSConfig() if err != nil { return nil, err @@ -54,10 +53,10 @@ func NewGRPCClient(c *Config) (Client, error) { cos := []grpc.CallOption{ grpc.PerRPCCredentials(&Credentials{ TokenSource: transport.NewCachedTokenSource(oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: c.Token, + AccessToken: c.Transport.BearerToken, })), - ImpersonationConfig: c.ImpersonationConfig, - SkipTransportSecurity: u.Scheme != "https", + ImpersonationConfig: &c.Transport.Impersonate, + SkipTransportSecurity: c.URL.Scheme != "https", }), } @@ -66,7 +65,7 @@ func NewGRPCClient(c *Config) (Client, error) { grpc.WithTransportCredentials(tc), } - clientConn, err := grpc.DialContext(ctx, u.Host, dos...) + clientConn, err := grpc.DialContext(ctx, c.URL.Host, dos...) if err != nil { return nil, err } @@ -79,18 +78,18 @@ func NewGRPCClient(c *Config) (Client, error) { func (c *Config) ClientTLSConfig() (*tls.Config, error) { tc := &tls.Config{ - InsecureSkipVerify: c.TLSConfig.Insecure, + InsecureSkipVerify: c.Transport.TLS.Insecure, } - if c.TLSConfig.CertFile != "" && c.TLSConfig.KeyFile != "" { - keyPair, err := tls.LoadX509KeyPair(c.TLSConfig.CertFile, c.TLSConfig.KeyFile) + if c.Transport.TLS.CertFile != "" && c.Transport.TLS.KeyFile != "" { + keyPair, err := tls.LoadX509KeyPair(c.Transport.TLS.CertFile, c.Transport.TLS.KeyFile) if err != nil { return nil, fmt.Errorf("could not load client key pair: %v", err) } tc.Certificates = []tls.Certificate{keyPair} - } else if c.TLSConfig.CAFile != "" { + } else if c.Transport.TLS.CAFile != "" { cp := x509.NewCertPool() - ca, err := os.ReadFile(c.TLSConfig.CAFile) + ca, err := os.ReadFile(c.Transport.TLS.CAFile) if err != nil { return nil, fmt.Errorf("could not read CA certificate: %v", err) } diff --git a/internal/results/client/rest.go b/internal/results/client/rest.go index f7e572b3..50801773 100644 --- a/internal/results/client/rest.go +++ b/internal/results/client/rest.go @@ -3,6 +3,7 @@ package client import ( "bytes" "context" + "errors" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" v1alpha2 "github.com/tektoncd/results/proto/v1alpha2/results_go_proto" "google.golang.org/genproto/googleapis/api/httpbody" @@ -15,44 +16,34 @@ import ( "k8s.io/client-go/transport" "net/http" "net/url" + "path" ) const ( - basePath = "/apis/results.tekton.dev/v1alpha2/parents" + pathPrefix = "parents" ) type RESTClient struct { - httpClient *http.Client - url *url.URL + url *url.URL + client *http.Client } // NewRESTClient creates a new REST client. func NewRESTClient(c *Config) (Client, error) { - u, err := url.Parse(c.Host) - if err != nil { - return nil, err - } - - u.Path = basePath + c.URL.Path = path.Join(c.URL.Path, pathPrefix) - rt, err := transport.New(&transport.Config{ - BearerToken: c.Token, - TLS: *c.TLSConfig, - Impersonate: *c.ImpersonationConfig, - }) + rt, err := transport.New(c.Transport) if err != nil { return nil, err } - rc := &RESTClient{ - httpClient: &http.Client{ + return &RESTClient{ + url: c.URL, + client: &http.Client{ Transport: rt, Timeout: c.Timeout, }, - url: u, - } - - return rc, nil + }, nil } // TODO: Get these methods from a generated client @@ -216,7 +207,7 @@ func (c *RESTClient) send(ctx context.Context, method string, values []string, i return nil, err } - res, err := c.httpClient.Do(req) + res, err := c.client.Do(req) if err != nil { return nil, err } @@ -224,7 +215,7 @@ func (c *RESTClient) send(ctx context.Context, method string, values []string, i if res.StatusCode != http.StatusOK { return nil, &runtime.HTTPStatusError{ HTTPStatus: res.StatusCode, - Err: err, + Err: errors.New(res.Status), } } diff --git a/internal/results/config/config.go b/internal/results/config/config.go index 3ed1a044..0a63bbd3 100644 --- a/internal/results/config/config.go +++ b/internal/results/config/config.go @@ -1,21 +1,17 @@ package config import ( - "context" + "encoding/json" "errors" "github.com/AlecAivazis/survey/v2" - jsoniter "github.com/json-iterator/go" - v1 "github.com/openshift/api/route/v1" - routev1 "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" "github.com/sayan-biswas/kubectl-tekton/internal/results/client" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" - "k8s.io/client-go/transport" "k8s.io/kubectl/pkg/cmd/util" + "path" "reflect" "strconv" "strings" @@ -24,7 +20,7 @@ import ( type Config interface { Get() *client.Config - RawConfig() runtime.Object + GetObject() runtime.Object Set(data map[string]*string, prompt bool) error Reset() error } @@ -40,6 +36,10 @@ type config struct { const ( ServiceLabel string = "app.kubernetes.io/name=tekton-results-api" ExtensionName string = "tekton-results" + Group string = "results.tekton.dev" + Version string = "v1alpha2" + Kind string = "Client" + Path string = "apis" ) func NewConfig(factory util.Factory) (Config, error) { @@ -67,23 +67,14 @@ func NewConfig(factory util.Factory) (Config, error) { return nil, err } - if c.Extension == nil { - c.SetVersion() - if err := c.Set(nil, true); err != nil { - return nil, err - } - } - - c.LoadClientConfig() - - return c, nil + return c, c.LoadClientConfig() } func (c *config) Get() *client.Config { return c.ClientConfig } -func (c *config) RawConfig() runtime.Object { +func (c *config) GetObject() runtime.Object { return c.Extension } @@ -100,14 +91,19 @@ func (c *config) LoadExtension() error { } c.Extension = new(Extension) e := cc.Extensions[ExtensionName] - return jsoniter.Unmarshal(e.(*runtime.Unknown).Raw, c.Extension) + if e == nil { + c.SetVersion() + return c.Set(nil, false) + } + return json.Unmarshal(e.(*runtime.Unknown).Raw, c.Extension) } func (c *config) SetVersion() { - c.Extension.TypeMeta = runtime.TypeMeta{ - APIVersion: "v1alpha1", - Kind: "Client", - } + c.Extension.TypeMeta.SetGroupVersionKind(schema.GroupVersionKind{ + Group: Group, + Version: Version, + Kind: Kind, + }) } func (c *config) Set(data map[string]*string, prompt bool) error { @@ -161,55 +157,75 @@ func (c *config) Set(data map[string]*string, prompt bool) error { return c.Persist() } -func (c *config) LoadClientConfig() { - ic := transport.ImpersonationConfig(c.RESTConfig.Impersonate) - c.ClientConfig = &client.Config{ - ClientType: client.REST, - Host: c.RESTConfig.Host, - ImpersonationConfig: &ic, - Timeout: c.RESTConfig.Timeout, - Token: c.RESTConfig.BearerToken, - TLSConfig: &transport.TLSConfig{ - Insecure: c.RESTConfig.Insecure, - CAFile: c.RESTConfig.CAFile, - CertFile: c.RESTConfig.CertFile, - KeyFile: c.RESTConfig.KeyFile, - ServerName: c.RESTConfig.ServerName, - }, - } +func (c *config) LoadClientConfig() error { + rc := rest.CopyConfig(c.RESTConfig) + + gv := c.Extension.TypeMeta.GroupVersionKind().GroupVersion() + rc.GroupVersion = &gv if c.Extension.Host != "" { - c.ClientConfig.Host = c.Extension.Host + rc.Host = c.Extension.Host + } + + if c.Extension.APIPath != "" { + rc.APIPath = c.Extension.APIPath } if c.Extension.Token != "" { - c.ClientConfig.Token = c.Extension.Token + rc.BearerToken = c.Extension.Token + } + + if c.Extension.TLSServerName != "" { + rc.TLSClientConfig.ServerName = c.Extension.TLSServerName + } + + if i, err := strconv.ParseBool(c.Extension.InsecureSkipTLSVerify); err == nil { + if i { + rc.TLSClientConfig = rest.TLSClientConfig{} + } + rc.Insecure = i } if d, err := time.ParseDuration(c.Extension.Timeout); err != nil { - c.ClientConfig.Timeout = d + rc.Timeout = d } if c.Extension.Impersonate != "" { - c.ClientConfig.ImpersonationConfig = &transport.ImpersonationConfig{ + rc.Impersonate = rest.ImpersonationConfig{ UserName: c.Extension.Impersonate, UID: c.Extension.ImpersonateUID, Groups: strings.Split(c.Extension.ImpersonateGroups, ","), } } - if i, err := strconv.ParseBool(c.Extension.InsecureSkipTLSVerify); err == nil { - c.ClientConfig.TLSConfig.Insecure = i + if c.Extension.CertificateAuthority != "" || c.Extension.ClientCertificate != "" || c.Extension.ClientKey != "" { + rc.TLSClientConfig = rest.TLSClientConfig{ + CAFile: c.Extension.CertificateAuthority, + CertFile: c.Extension.ClientCertificate, + KeyFile: c.Extension.ClientKey, + } } - if c.Extension.CertificateAuthority != "" || c.Extension.ClientCertificate != "" { - c.ClientConfig.TLSConfig = &transport.TLSConfig{ - CAFile: c.Extension.CertificateAuthority, - CertFile: c.Extension.ClientCertificate, - KeyFile: c.Extension.ClientKey, - ServerName: c.Extension.TLSServerName, - } + tc, err := rc.TransportConfig() + if err != nil { + return err + } + + rc.APIPath = path.Join(rc.APIPath, Path) + u, p, err := rest.DefaultServerUrlFor(rc) + if err != nil { + return err } + u.Path = p + + c.ClientConfig = &client.Config{ + Transport: tc, + URL: u, + Timeout: c.RESTConfig.Timeout, + ClientType: c.Extension.ClientType, + } + + return nil } func (c *config) CallMethod(name string) any { @@ -266,7 +282,7 @@ func (c *config) InsecureSkipTLSVerify() any { } func (c *config) Host() any { - routes, err := c.routes() + routes, err := getRoutes(c.RESTConfig) if err != nil { return err } @@ -281,52 +297,3 @@ func (c *config) Host() any { } return hosts } - -func (c *config) routes() ([]*v1.Route, error) { - coreV1Client, err := corev1.NewForConfig(c.RESTConfig) - if err != nil { - return nil, err - } - - routeV1Client, err := routev1.NewForConfig(c.RESTConfig) - if err != nil { - return nil, err - } - - ctx := context.Background() - - serviceList, err := coreV1Client. - Services(""). - List(ctx, metav1.ListOptions{ - LabelSelector: ServiceLabel, - }) - if err != nil { - return nil, err - } - if len(serviceList.Items) == 0 { - return nil, errors.New("services for tekton results not found, try manual configuration") - } - - var routes []*v1.Route - for _, service := range serviceList.Items { - routeList, err := routeV1Client.Routes(service.Namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - if len(routeList.Items) == 0 { - return nil, errors.New("routes for tekton results not found, try manual configuration") - } - - for _, route := range routeList.Items { - if route.Spec.To.Name == service.Name { - port := route.Spec.Port.TargetPort - for _, p := range service.Spec.Ports { - if p.Port == port.IntVal || p.Name == port.StrVal { - routes = append(routes, &route) - } - } - } - } - } - return routes, nil -} diff --git a/internal/results/config/extension.go b/internal/results/config/extension.go index cf888399..166ef20d 100644 --- a/internal/results/config/extension.go +++ b/internal/results/config/extension.go @@ -9,9 +9,11 @@ import ( // Extension stores the information about results type Extension struct { runtime.TypeMeta `json:",inline"` - Host string `json:"host,omitempty" group:"client"` - Token string `json:"token,omitempty" group:"auth"` ClientType string `json:"client-type,omitempty" group:"client"` + Host string `json:"host,omitempty" group:"client"` + APIPath string `json:"api-path,omitempty" group:"client"` + InsecureSkipTLSVerify string `json:"insecure-skip-tls-verify,omitempty" group:"client"` + Timeout string `json:"timeout,omitempty" group:"client"` CertificateAuthority string `json:"certificate-authority,omitempty" group:"tls"` ClientCertificate string `json:"client-certificate,omitempty" group:"tls"` ClientKey string `json:"client-key,omitempty" group:"tls"` @@ -19,8 +21,7 @@ type Extension struct { Impersonate string `json:"act-as,omitempty" group:"auth"` ImpersonateUID string `json:"act-as-uid,omitempty" group:"auth"` ImpersonateGroups string `json:"act-as-groups,omitempty" group:"auth"` - InsecureSkipTLSVerify string `json:"insecure-skip-tls-verify,omitempty" group:"client"` - Timeout string `json:"timeout,omitempty" group:"client"` + Token string `json:"token,omitempty" group:"auth"` } // DeepCopy is an autogenerated deep copy function, copying the receiver, creating a new Extension. diff --git a/internal/results/config/host.go b/internal/results/config/host.go new file mode 100644 index 00000000..9d330d51 --- /dev/null +++ b/internal/results/config/host.go @@ -0,0 +1,60 @@ +package config + +import ( + "context" + "errors" + v1 "github.com/openshift/api/route/v1" + routev1 "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" +) + +func getRoutes(c *rest.Config) ([]*v1.Route, error) { + coreV1Client, err := corev1.NewForConfig(c) + if err != nil { + return nil, err + } + + routeV1Client, err := routev1.NewForConfig(c) + if err != nil { + return nil, err + } + + ctx := context.Background() + + serviceList, err := coreV1Client. + Services(""). + List(ctx, metav1.ListOptions{ + LabelSelector: ServiceLabel, + }) + if err != nil { + return nil, err + } + if len(serviceList.Items) == 0 { + return nil, errors.New("services for tekton results not found, try manual configuration") + } + + var routes []*v1.Route + for _, service := range serviceList.Items { + routeList, err := routeV1Client.Routes(service.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + if len(routeList.Items) == 0 { + return nil, errors.New("routes for tekton results not found, try manual configuration") + } + + for _, route := range routeList.Items { + if route.Spec.To.Name == service.Name { + port := route.Spec.Port.TargetPort + for _, p := range service.Spec.Ports { + if p.Port == port.IntVal || p.Name == port.StrVal { + routes = append(routes, &route) + } + } + } + } + } + return routes, nil +}