From b9fc1e3d5017336f1c36944ec09ebc233704f821 Mon Sep 17 00:00:00 2001 From: Khaled Emara Date: Mon, 11 Mar 2024 09:30:29 +0200 Subject: [PATCH] chore(apicall): remove duplicate code (#9880) Signed-off-by: Khaled Emara --- pkg/engine/apicall/apiCall.go | 177 ++---------------- pkg/engine/apicall/apiCall_test.go | 4 +- pkg/engine/apicall/{caller.go => executor.go} | 116 +++++++----- pkg/globalcontext/externalapi/entry.go | 4 +- 4 files changed, 86 insertions(+), 215 deletions(-) rename pkg/engine/apicall/{caller.go => executor.go} (81%) diff --git a/pkg/engine/apicall/apiCall.go b/pkg/engine/apicall/apiCall.go index b29f3de4a8..886429b3e4 100644 --- a/pkg/engine/apicall/apiCall.go +++ b/pkg/engine/apicall/apiCall.go @@ -1,32 +1,23 @@ package apicall import ( - "bytes" "context" - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" - "io" - "net/http" - "os" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/variables" - "github.com/kyverno/kyverno/pkg/tracing" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) type apiCall struct { - logger logr.Logger - jp jmespath.Interface - entry kyvernov1.ContextEntry - jsonCtx enginecontext.Interface - client ClientInterface - config APICallConfiguration + logger logr.Logger + jp jmespath.Interface + entry kyvernov1.ContextEntry + jsonCtx enginecontext.Interface + executor Executor } func New( @@ -40,13 +31,15 @@ func New( if entry.APICall == nil { return nil, fmt.Errorf("missing APICall in context entry %v", entry) } + + executor := NewExecutor(logger, entry.Name, client, apiCallConfig) + return &apiCall{ - logger: logger, - jp: jp, - entry: entry, - jsonCtx: jsonCtx, - client: client, - config: apiCallConfig, + logger: logger, + jp: jp, + entry: entry, + jsonCtx: jsonCtx, + executor: executor, }, nil } @@ -69,7 +62,7 @@ func (a *apiCall) Fetch(ctx context.Context) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to substitute variables in context entry %s %s: %v", a.entry.Name, a.entry.APICall.URLPath, err) } - data, err := a.Execute(ctx, call) + data, err := a.Execute(ctx, &call.APICall) if err != nil { return nil, err } @@ -84,146 +77,8 @@ func (a *apiCall) Store(data []byte) ([]byte, error) { return results, nil } -func (a *apiCall) Execute(ctx context.Context, call *kyvernov1.ContextAPICall) ([]byte, error) { - if call.URLPath != "" { - return a.executeK8sAPICall(ctx, call.URLPath, call.Method, call.Data) - } - return a.executeServiceCall(ctx, call) -} - -func (a *apiCall) executeK8sAPICall(ctx context.Context, path string, method kyvernov1.Method, data []kyvernov1.RequestData) ([]byte, error) { - requestData, err := a.buildRequestData(data) - if err != nil { - return nil, err - } - jsonData, err := a.client.RawAbsPath(ctx, path, string(method), requestData) - if err != nil { - return nil, fmt.Errorf("failed to %v resource with raw url\n: %s: %v", method, path, err) - } - a.logger.V(4).Info("executed APICall", "name", a.entry.Name, "path", path, "method", method, "len", len(jsonData)) - return jsonData, nil -} - -func (a *apiCall) executeServiceCall(ctx context.Context, apiCall *kyvernov1.ContextAPICall) ([]byte, error) { - if apiCall.Service == nil { - return nil, fmt.Errorf("missing service for APICall %s", a.entry.Name) - } - - client, err := a.buildHTTPClient(apiCall.Service) - if err != nil { - return nil, err - } - - req, err := a.buildHTTPRequest(ctx, apiCall) - if err != nil { - return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.entry.Name, err) - } - - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to execute HTTP request for APICall %s: %w", a.entry.Name, err) - } - defer resp.Body.Close() - var w http.ResponseWriter - - if a.config.maxAPICallResponseLength != 0 { - resp.Body = http.MaxBytesReader(w, resp.Body, a.config.maxAPICallResponseLength) - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - b, err := io.ReadAll(resp.Body) - if err == nil { - return nil, fmt.Errorf("HTTP %s: %s", resp.Status, string(b)) - } - - return nil, fmt.Errorf("HTTP %s", resp.Status) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - if _, ok := err.(*http.MaxBytesError); ok { - return nil, fmt.Errorf("response length must be less than max allowed response length of %d.", a.config.maxAPICallResponseLength) - } else { - return nil, fmt.Errorf("failed to read data from APICall %s: %w", a.entry.Name, err) - } - } - - a.logger.Info("executed service APICall", "name", a.entry.Name, "len", len(body)) - return body, nil -} - -func (a *apiCall) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.ContextAPICall) (req *http.Request, err error) { - if apiCall.Service == nil { - return nil, fmt.Errorf("missing service") - } - - token := a.getToken() - defer func() { - if token != "" && req != nil { - req.Header.Add("Authorization", "Bearer "+token) - } - }() - - if apiCall.Method == "GET" { - req, err = http.NewRequestWithContext(ctx, "GET", apiCall.Service.URL, nil) - return - } - - if apiCall.Method == "POST" { - data, dataErr := a.buildRequestData(apiCall.Data) - if dataErr != nil { - return nil, dataErr - } - - req, err = http.NewRequest("POST", apiCall.Service.URL, data) - return - } - - return nil, fmt.Errorf("invalid request type %s for APICall %s", apiCall.Method, a.entry.Name) -} - -func (a *apiCall) getToken() string { - fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" - b, err := os.ReadFile(fileName) - if err != nil { - a.logger.Info("failed to read service account token", "path", fileName) - return "" - } - - return string(b) -} - -func (a *apiCall) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) { - if service == nil || service.CABundle == "" { - return http.DefaultClient, nil - } - caCertPool := x509.NewCertPool() - if ok := caCertPool.AppendCertsFromPEM([]byte(service.CABundle)); !ok { - return nil, fmt.Errorf("failed to parse PEM CA bundle for APICall %s", a.entry.Name) - } - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: caCertPool, - MinVersion: tls.VersionTLS12, - }, - } - return &http.Client{ - Transport: tracing.Transport(transport, otelhttp.WithFilter(tracing.RequestFilterIsInSpan)), - }, nil -} - -func (a *apiCall) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) { - dataMap := make(map[string]interface{}) - for _, d := range data { - dataMap[d.Key] = d.Value - } - - buffer := new(bytes.Buffer) - if err := json.NewEncoder(buffer).Encode(dataMap); err != nil { - return nil, fmt.Errorf("failed to encode HTTP POST data %v for APICall %s: %w", dataMap, a.entry.Name, err) - } - - return buffer, nil +func (a *apiCall) Execute(ctx context.Context, call *kyvernov1.APICall) ([]byte, error) { + return a.executor.Execute(ctx, call) } func (a *apiCall) transformAndStore(jsonData []byte) ([]byte, error) { diff --git a/pkg/engine/apicall/apiCall_test.go b/pkg/engine/apicall/apiCall_test.go index cb82be27b5..3034a9a3db 100644 --- a/pkg/engine/apicall/apiCall_test.go +++ b/pkg/engine/apicall/apiCall_test.go @@ -103,8 +103,8 @@ func Test_serviceGetRequest(t *testing.T) { call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed) assert.NilError(t, err) - data, err = call.FetchAndLoad(context.TODO()) - assert.ErrorContains(t, err, "response length must be less than max allowed response length of 10.") + _, err = call.FetchAndLoad(context.TODO()) + assert.ErrorContains(t, err, "response length must be less than max allowed response length of 10") call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck) assert.NilError(t, err) diff --git a/pkg/engine/apicall/caller.go b/pkg/engine/apicall/executor.go similarity index 81% rename from pkg/engine/apicall/caller.go rename to pkg/engine/apicall/executor.go index 0b64eb8012..e43509ea5e 100644 --- a/pkg/engine/apicall/caller.go +++ b/pkg/engine/apicall/executor.go @@ -17,39 +17,39 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -type Caller interface { +type Executor interface { Execute(context.Context, *kyvernov1.APICall) ([]byte, error) } -type caller struct { +type executor struct { logger logr.Logger name string client ClientInterface config APICallConfiguration } -func NewCaller( +func NewExecutor( logger logr.Logger, name string, client ClientInterface, - config APICallConfiguration, -) *caller { - return &caller{ + apiCallConfig APICallConfiguration, +) *executor { + return &executor{ logger: logger, name: name, client: client, - config: config, + config: apiCallConfig, } } -func (a *caller) Execute(ctx context.Context, call *kyvernov1.APICall) ([]byte, error) { +func (a *executor) Execute(ctx context.Context, call *kyvernov1.APICall) ([]byte, error) { if call.URLPath != "" { return a.executeK8sAPICall(ctx, call.URLPath, call.Method, call.Data) } return a.executeServiceCall(ctx, call) } -func (a *caller) executeK8sAPICall(ctx context.Context, path string, method kyvernov1.Method, data []kyvernov1.RequestData) ([]byte, error) { +func (a *executor) executeK8sAPICall(ctx context.Context, path string, method kyvernov1.Method, data []kyvernov1.RequestData) ([]byte, error) { requestData, err := a.buildRequestData(data) if err != nil { return nil, err @@ -62,59 +62,96 @@ func (a *caller) executeK8sAPICall(ctx context.Context, path string, method kyve return jsonData, nil } -func (a *caller) executeServiceCall(ctx context.Context, apiCall *kyvernov1.APICall) ([]byte, error) { +func (a *executor) executeServiceCall(ctx context.Context, apiCall *kyvernov1.APICall) ([]byte, error) { if apiCall.Service == nil { return nil, fmt.Errorf("missing service for APICall %s", a.name) } + client, err := a.buildHTTPClient(apiCall.Service) if err != nil { return nil, err } + req, err := a.buildHTTPRequest(ctx, apiCall) if err != nil { return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.name, err) } + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute HTTP request for APICall %s: %w", a.name, err) } defer resp.Body.Close() var w http.ResponseWriter + if a.config.maxAPICallResponseLength != 0 { resp.Body = http.MaxBytesReader(w, resp.Body, a.config.maxAPICallResponseLength) } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, err := io.ReadAll(resp.Body) if err == nil { return nil, fmt.Errorf("HTTP %s: %s", resp.Status, string(b)) } + return nil, fmt.Errorf("HTTP %s", resp.Status) } + body, err := io.ReadAll(resp.Body) if err != nil { if _, ok := err.(*http.MaxBytesError); ok { - return nil, fmt.Errorf("response length must be less than max allowed response length of %d.", a.config.maxAPICallResponseLength) + return nil, fmt.Errorf("response length must be less than max allowed response length of %d", a.config.maxAPICallResponseLength) } else { return nil, fmt.Errorf("failed to read data from APICall %s: %w", a.name, err) } } + a.logger.Info("executed service APICall", "name", a.name, "len", len(body)) return body, nil } -func (a *caller) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) { - dataMap := make(map[string]interface{}) - for _, d := range data { - dataMap[d.Key] = d.Value +func (a *executor) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.APICall) (req *http.Request, err error) { + if apiCall.Service == nil { + return nil, fmt.Errorf("missing service") } - buffer := new(bytes.Buffer) - if err := json.NewEncoder(buffer).Encode(dataMap); err != nil { - return nil, fmt.Errorf("failed to encode HTTP POST data %v for APICall %s: %w", dataMap, a.name, err) + + token := a.getToken() + defer func() { + if token != "" && req != nil { + req.Header.Add("Authorization", "Bearer "+token) + } + }() + + if apiCall.Method == "GET" { + req, err = http.NewRequestWithContext(ctx, "GET", apiCall.Service.URL, nil) + return } - return buffer, nil + + if apiCall.Method == "POST" { + data, dataErr := a.buildRequestData(apiCall.Data) + if dataErr != nil { + return nil, dataErr + } + + req, err = http.NewRequest("POST", apiCall.Service.URL, data) + return + } + + return nil, fmt.Errorf("invalid request type %s for APICall %s", apiCall.Method, a.name) } -func (a *caller) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) { +func (a *executor) getToken() string { + fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" + b, err := os.ReadFile(fileName) + if err != nil { + a.logger.Info("failed to read service account token", "path", fileName) + return "" + } + + return string(b) +} + +func (a *executor) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) { if service == nil || service.CABundle == "" { return http.DefaultClient, nil } @@ -133,37 +170,16 @@ func (a *caller) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, }, nil } -func (a *caller) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.APICall) (req *http.Request, err error) { - if apiCall.Service == nil { - return nil, fmt.Errorf("missing service") +func (a *executor) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) { + dataMap := make(map[string]interface{}) + for _, d := range data { + dataMap[d.Key] = d.Value } - token := a.getToken() - defer func() { - if token != "" && req != nil { - req.Header.Add("Authorization", "Bearer "+token) - } - }() - if apiCall.Method == "GET" { - req, err = http.NewRequestWithContext(ctx, "GET", apiCall.Service.URL, nil) - return - } - if apiCall.Method == "POST" { - data, dataErr := a.buildRequestData(apiCall.Data) - if dataErr != nil { - return nil, dataErr - } - req, err = http.NewRequest("POST", apiCall.Service.URL, data) - return - } - return nil, fmt.Errorf("invalid request type %s for APICall %s", apiCall.Method, a.name) -} -func (a *caller) getToken() string { - fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" - b, err := os.ReadFile(fileName) - if err != nil { - a.logger.Info("failed to read service account token", "path", fileName) - return "" + buffer := new(bytes.Buffer) + if err := json.NewEncoder(buffer).Encode(dataMap); err != nil { + return nil, fmt.Errorf("failed to encode HTTP POST data %v for APICall %s: %w", dataMap, a.name, err) } - return string(b) + + return buffer, nil } diff --git a/pkg/globalcontext/externalapi/entry.go b/pkg/globalcontext/externalapi/entry.go index e5a3d7ca73..d2f142f2d6 100644 --- a/pkg/globalcontext/externalapi/entry.go +++ b/pkg/globalcontext/externalapi/entry.go @@ -56,7 +56,7 @@ func New( group.StartWithContext(ctx, func(ctx context.Context) { config := apicall.NewAPICallConfiguration(maxResponseLength) - caller := apicall.NewCaller(logger, "globalcontext", client, config) + caller := apicall.NewExecutor(logger, "globalcontext", client, config) wait.UntilWithContext(ctx, func(ctx context.Context) { if data, err := doCall(ctx, caller, call); err != nil { @@ -124,7 +124,7 @@ func (e *entry) setData(data any, err error) { } } -func doCall(ctx context.Context, caller apicall.Caller, call kyvernov1.APICall) (any, error) { +func doCall(ctx context.Context, caller apicall.Executor, call kyvernov1.APICall) (any, error) { return caller.Execute(ctx, &call) }