From bffe907bdc76539ca46ca4650d5d9fe0246d05ca Mon Sep 17 00:00:00 2001 From: Wojciech Trocki Date: Wed, 30 Mar 2022 11:56:36 +0100 Subject: [PATCH] fix: add support for calling any endpoint from control plane (#1497) --- pkg/api/generic/api.go | 106 ++++++++++++++++++ pkg/api/rbac/api.go | 5 +- pkg/cmd/request/request.go | 88 +++++++++++++++ pkg/cmd/root/root.go | 2 + pkg/shared/connection/api/api.go | 2 + .../api/defaultapi/default_client.go | 12 ++ 6 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 pkg/api/generic/api.go create mode 100644 pkg/cmd/request/request.go diff --git a/pkg/api/generic/api.go b/pkg/api/generic/api.go new file mode 100644 index 000000000..93b95ae6f --- /dev/null +++ b/pkg/api/generic/api.go @@ -0,0 +1,106 @@ +package generic + +import ( + "context" + "errors" + "io" + "net/http" + "strings" +) + +type GenericAPI interface { + GET(ctx context.Context, path string) (interface{}, *http.Response, error) + POST(ctx context.Context, path string, body io.Reader) (interface{}, *http.Response, error) +} + +// APIConfig defines the available configuration options +// to customize the API client settings +type Config struct { + // HTTPClient is a custom HTTP client + HTTPClient *http.Client + // Debug enables debug-level logging + Debug bool + // BaseURL sets a custom API server base URL + BaseURL string +} + +func NewGenericAPIClient(cfg *Config) GenericAPI { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := APIClient{ + baseURL: cfg.BaseURL, + httpClient: cfg.HTTPClient, + } + + return &c +} + +type APIClient struct { + httpClient *http.Client + baseURL string +} + +func (c *APIClient) GET(ctx context.Context, path string) (interface{}, *http.Response, error) { + url := c.baseURL + path + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err, nil, err + } + + req = req.WithContext(ctx) + + req.Header.Set("Accept", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, resp, err + } + if resp.StatusCode > http.StatusBadRequest { + return nil, resp, errors.New(resp.Status) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, resp, err + } + + return string(b), resp, err +} + +func (c *APIClient) POST(ctx context.Context, path string, body io.Reader) (interface{}, *http.Response, error) { + url := c.baseURL + path + bodyBinary, err := io.ReadAll(body) + + if err != nil { + return err, nil, err + } + requestBody := strings.NewReader(string(bodyBinary)) + req, err := http.NewRequest("POST", url, requestBody) + if err != nil { + return nil, nil, err + } + + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, resp, err + } + if resp.StatusCode > http.StatusBadRequest { + return nil, resp, errors.New(resp.Status) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, resp, err + } + + return string(b), resp, err +} diff --git a/pkg/api/rbac/api.go b/pkg/api/rbac/api.go index 717926c9f..279015ad0 100644 --- a/pkg/api/rbac/api.go +++ b/pkg/api/rbac/api.go @@ -68,9 +68,8 @@ func NewPrincipalAPIClient(cfg *Config) PrincipalAPI { } type APIClient struct { - httpClient *http.Client - baseURL *url.URL - AccessToken string + httpClient *http.Client + baseURL *url.URL } // GetPrincipals returns the list of user's in the current users organization/tenant diff --git a/pkg/cmd/request/request.go b/pkg/cmd/request/request.go new file mode 100644 index 000000000..8e732c242 --- /dev/null +++ b/pkg/cmd/request/request.go @@ -0,0 +1,88 @@ +package request + +import ( + "context" + "errors" + "fmt" + + "github.com/redhat-developer/app-services-cli/pkg/cmd/registry/artifact/util" + "github.com/redhat-developer/app-services-cli/pkg/core/ioutil/iostreams" + "github.com/redhat-developer/app-services-cli/pkg/core/localize" + "github.com/redhat-developer/app-services-cli/pkg/core/logging" + "github.com/redhat-developer/app-services-cli/pkg/shared/connection" + "github.com/redhat-developer/app-services-cli/pkg/shared/factory" + "github.com/spf13/cobra" +) + +type options struct { + IO *iostreams.IOStreams + Logger logging.Logger + localizer localize.Localizer + Context context.Context + Connection factory.ConnectionFunc + + urlPath string + method string +} + +func NewCallCmd(f *factory.Factory) *cobra.Command { + opts := &options{ + IO: f.IOStreams, + Logger: f.Logger, + localizer: f.Localizer, + Context: f.Context, + Connection: f.Connection, + } + + cmd := &cobra.Command{ + Use: "request", + Short: "Allows you to perform API requests against the API server", + Example: ` + # Perform a GET request to the specified path + rhoas request --path /api/kafkas_mgmt/v1/kafkas + + # Perform a POST request to the specified path + cat request.json | rhoas request --path "/api/kafkas_mgmt/v1/kafkas?async=true" --method post `, + Hidden: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runCmd(opts) + }, + } + cmd.Flags().StringVar(&opts.urlPath, "path", "", "Path to send request. For example /api/kafkas_mgmt/v1/kafkas?async=true") + cmd.Flags().StringVar(&opts.method, "method", "GET", "HTTP method to use. (get, post)") + return cmd +} + +func runCmd(opts *options) (err error) { + if opts.urlPath == "" { + return errors.New("--path is required") + } + opts.Logger.Info("Performing request to", opts.urlPath) + conn, err := opts.Connection(connection.DefaultConfigSkipMasAuth) + + if err != nil { + return err + } + + var data interface{} + var response interface{} + if opts.method == "post" { + opts.Logger.Info("POST request. Reading file from standard input") + specifiedFile, err1 := util.CreateFileFromStdin() + if err1 != nil { + return err + } + data, response, err = conn.API().GenericAPI().POST(opts.Context, opts.urlPath, specifiedFile) + } else { + data, response, err = conn.API().GenericAPI().GET(opts.Context, opts.urlPath) + } + + if err != nil || data == nil { + opts.Logger.Info("Fetching data failed", err, response) + return err + } + + fmt.Fprint(opts.IO.Out, data) + return nil +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 3dd53eba5..d9fe8a3a3 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -8,6 +8,7 @@ import ( "github.com/redhat-developer/app-services-cli/pkg/cmd/login" "github.com/redhat-developer/app-services-cli/pkg/cmd/logout" "github.com/redhat-developer/app-services-cli/pkg/cmd/registry" + "github.com/redhat-developer/app-services-cli/pkg/cmd/request" "github.com/redhat-developer/app-services-cli/pkg/cmd/serviceaccount" "github.com/redhat-developer/app-services-cli/pkg/cmd/status" cliversion "github.com/redhat-developer/app-services-cli/pkg/cmd/version" @@ -53,6 +54,7 @@ func NewRootCommand(f *factory.Factory, version string) *cobra.Command { cmd.AddCommand(registry.NewServiceRegistryCommand(f)) cmd.AddCommand(docs.NewDocsCmd(f)) + cmd.AddCommand(request.NewCallCmd(f)) return cmd } diff --git a/pkg/shared/connection/api/api.go b/pkg/shared/connection/api/api.go index 382a7c764..8e3e497e4 100644 --- a/pkg/shared/connection/api/api.go +++ b/pkg/shared/connection/api/api.go @@ -1,6 +1,7 @@ package api import ( + "github.com/redhat-developer/app-services-cli/pkg/api/generic" "github.com/redhat-developer/app-services-cli/pkg/api/rbac" amsclient "github.com/redhat-developer/app-services-sdk-go/accountmgmt/apiv1/client" kafkainstanceclient "github.com/redhat-developer/app-services-sdk-go/kafkainstance/apiv1internal/client" @@ -17,4 +18,5 @@ type API interface { ServiceRegistryInstance(instanceID string) (*registryinstanceclient.APIClient, *registrymgmtclient.Registry, error) AccountMgmt() amsclient.AppServicesApi RBAC() rbac.RbacAPI + GenericAPI() generic.GenericAPI } diff --git a/pkg/shared/connection/api/defaultapi/default_client.go b/pkg/shared/connection/api/defaultapi/default_client.go index eb6de48a1..ae9ecc9b0 100644 --- a/pkg/shared/connection/api/defaultapi/default_client.go +++ b/pkg/shared/connection/api/defaultapi/default_client.go @@ -12,6 +12,7 @@ import ( "github.com/redhat-developer/app-services-cli/pkg/shared/kafkautil" "github.com/redhat-developer/app-services-cli/internal/build" + "github.com/redhat-developer/app-services-cli/pkg/api/generic" "github.com/redhat-developer/app-services-cli/pkg/api/rbac" "github.com/redhat-developer/app-services-cli/pkg/core/logging" "github.com/redhat-developer/app-services-cli/pkg/shared/connection/api" @@ -231,6 +232,17 @@ func (a *defaultAPI) ServiceRegistryInstance(instanceID string) (*registryinstan return client, &instance, nil } +func (a *defaultAPI) GenericAPI() generic.GenericAPI { + tc := a.createOAuthTransport(a.AccessToken) + client := generic.NewGenericAPIClient(&generic.Config{ + BaseURL: a.ApiURL.String(), + Debug: a.Logger.DebugEnabled(), + HTTPClient: tc, + }) + + return client +} + // AccountMgmt returns a new Account Management API client instance func (a *defaultAPI) AccountMgmt() amsclient.AppServicesApi { cfg := amsclient.NewConfiguration()