1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-15 12:17:56 +00:00

chore(apicall): remove duplicate code (#9880)

Signed-off-by: Khaled Emara <khaled.emara@nirmata.com>
This commit is contained in:
Khaled Emara 2024-03-11 09:30:29 +02:00 committed by GitHub
parent a3c64b3347
commit b9fc1e3d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 86 additions and 215 deletions

View file

@ -1,32 +1,23 @@
package apicall package apicall
import ( import (
"bytes"
"context" "context"
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
"os"
"github.com/go-logr/logr" "github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context" enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/jmespath"
"github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/tracing"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
) )
type apiCall struct { type apiCall struct {
logger logr.Logger logger logr.Logger
jp jmespath.Interface jp jmespath.Interface
entry kyvernov1.ContextEntry entry kyvernov1.ContextEntry
jsonCtx enginecontext.Interface jsonCtx enginecontext.Interface
client ClientInterface executor Executor
config APICallConfiguration
} }
func New( func New(
@ -40,13 +31,15 @@ func New(
if entry.APICall == nil { if entry.APICall == nil {
return nil, fmt.Errorf("missing APICall in context entry %v", entry) return nil, fmt.Errorf("missing APICall in context entry %v", entry)
} }
executor := NewExecutor(logger, entry.Name, client, apiCallConfig)
return &apiCall{ return &apiCall{
logger: logger, logger: logger,
jp: jp, jp: jp,
entry: entry, entry: entry,
jsonCtx: jsonCtx, jsonCtx: jsonCtx,
client: client, executor: executor,
config: apiCallConfig,
}, nil }, nil
} }
@ -69,7 +62,7 @@ func (a *apiCall) Fetch(ctx context.Context) ([]byte, error) {
if err != nil { 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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -84,146 +77,8 @@ func (a *apiCall) Store(data []byte) ([]byte, error) {
return results, nil return results, nil
} }
func (a *apiCall) Execute(ctx context.Context, call *kyvernov1.ContextAPICall) ([]byte, error) { func (a *apiCall) Execute(ctx context.Context, call *kyvernov1.APICall) ([]byte, error) {
if call.URLPath != "" { return a.executor.Execute(ctx, call)
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) transformAndStore(jsonData []byte) ([]byte, error) { func (a *apiCall) transformAndStore(jsonData []byte) ([]byte, error) {

View file

@ -103,8 +103,8 @@ func Test_serviceGetRequest(t *testing.T) {
call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed) call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed)
assert.NilError(t, err) assert.NilError(t, err)
data, err = call.FetchAndLoad(context.TODO()) _, err = call.FetchAndLoad(context.TODO())
assert.ErrorContains(t, err, "response length must be less than max allowed response length of 10.") 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) call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck)
assert.NilError(t, err) assert.NilError(t, err)

View file

@ -17,39 +17,39 @@ import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
) )
type Caller interface { type Executor interface {
Execute(context.Context, *kyvernov1.APICall) ([]byte, error) Execute(context.Context, *kyvernov1.APICall) ([]byte, error)
} }
type caller struct { type executor struct {
logger logr.Logger logger logr.Logger
name string name string
client ClientInterface client ClientInterface
config APICallConfiguration config APICallConfiguration
} }
func NewCaller( func NewExecutor(
logger logr.Logger, logger logr.Logger,
name string, name string,
client ClientInterface, client ClientInterface,
config APICallConfiguration, apiCallConfig APICallConfiguration,
) *caller { ) *executor {
return &caller{ return &executor{
logger: logger, logger: logger,
name: name, name: name,
client: client, 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 != "" { if call.URLPath != "" {
return a.executeK8sAPICall(ctx, call.URLPath, call.Method, call.Data) return a.executeK8sAPICall(ctx, call.URLPath, call.Method, call.Data)
} }
return a.executeServiceCall(ctx, call) 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) requestData, err := a.buildRequestData(data)
if err != nil { if err != nil {
return nil, err return nil, err
@ -62,59 +62,96 @@ func (a *caller) executeK8sAPICall(ctx context.Context, path string, method kyve
return jsonData, nil 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 { if apiCall.Service == nil {
return nil, fmt.Errorf("missing service for APICall %s", a.name) return nil, fmt.Errorf("missing service for APICall %s", a.name)
} }
client, err := a.buildHTTPClient(apiCall.Service) client, err := a.buildHTTPClient(apiCall.Service)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := a.buildHTTPRequest(ctx, apiCall) req, err := a.buildHTTPRequest(ctx, apiCall)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.name, err) return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.name, err)
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute HTTP request for APICall %s: %w", a.name, err) return nil, fmt.Errorf("failed to execute HTTP request for APICall %s: %w", a.name, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
var w http.ResponseWriter var w http.ResponseWriter
if a.config.maxAPICallResponseLength != 0 { if a.config.maxAPICallResponseLength != 0 {
resp.Body = http.MaxBytesReader(w, resp.Body, a.config.maxAPICallResponseLength) resp.Body = http.MaxBytesReader(w, resp.Body, a.config.maxAPICallResponseLength)
} }
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, err := io.ReadAll(resp.Body) b, err := io.ReadAll(resp.Body)
if err == nil { if err == nil {
return nil, fmt.Errorf("HTTP %s: %s", resp.Status, string(b)) return nil, fmt.Errorf("HTTP %s: %s", resp.Status, string(b))
} }
return nil, fmt.Errorf("HTTP %s", resp.Status) return nil, fmt.Errorf("HTTP %s", resp.Status)
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
if _, ok := err.(*http.MaxBytesError); ok { 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 { } else {
return nil, fmt.Errorf("failed to read data from APICall %s: %w", a.name, err) 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)) a.logger.Info("executed service APICall", "name", a.name, "len", len(body))
return body, nil return body, nil
} }
func (a *caller) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) { func (a *executor) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.APICall) (req *http.Request, err error) {
dataMap := make(map[string]interface{}) if apiCall.Service == nil {
for _, d := range data { return nil, fmt.Errorf("missing service")
dataMap[d.Key] = d.Value
} }
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(dataMap); err != nil { token := a.getToken()
return nil, fmt.Errorf("failed to encode HTTP POST data %v for APICall %s: %w", dataMap, a.name, err) 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 == "" { if service == nil || service.CABundle == "" {
return http.DefaultClient, nil return http.DefaultClient, nil
} }
@ -133,37 +170,16 @@ func (a *caller) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client,
}, nil }, nil
} }
func (a *caller) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.APICall) (req *http.Request, err error) { func (a *executor) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) {
if apiCall.Service == nil { dataMap := make(map[string]interface{})
return nil, fmt.Errorf("missing service") 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 { buffer := new(bytes.Buffer)
fileName := "/var/run/secrets/kubernetes.io/serviceaccount/token" if err := json.NewEncoder(buffer).Encode(dataMap); err != nil {
b, err := os.ReadFile(fileName) return nil, fmt.Errorf("failed to encode HTTP POST data %v for APICall %s: %w", dataMap, a.name, err)
if err != nil {
a.logger.Info("failed to read service account token", "path", fileName)
return ""
} }
return string(b)
return buffer, nil
} }

View file

@ -56,7 +56,7 @@ func New(
group.StartWithContext(ctx, func(ctx context.Context) { group.StartWithContext(ctx, func(ctx context.Context) {
config := apicall.NewAPICallConfiguration(maxResponseLength) 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) { wait.UntilWithContext(ctx, func(ctx context.Context) {
if data, err := doCall(ctx, caller, call); err != nil { 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) return caller.Execute(ctx, &call)
} }