From 6fdd6e2ed78e1b448a17de530bfbd83bd17c99ac Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Mon, 21 Oct 2024 14:58:02 -0400 Subject: [PATCH 1/2] feat(domains): api domain options --- src/pkg/common/schemas/validation.json | 37 ++++++++++++++++++++++++ src/pkg/domains/api/api.go | 32 +++++++++++++++++---- src/pkg/domains/api/spec.go | 29 +++++++++++++++++++ src/pkg/domains/api/types.go | 40 ++++++++++++++------------ 4 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 src/pkg/domains/api/spec.go diff --git a/src/pkg/common/schemas/validation.json b/src/pkg/common/schemas/validation.json index efabd43d..0e5bce56 100644 --- a/src/pkg/common/schemas/validation.json +++ b/src/pkg/common/schemas/validation.json @@ -362,8 +362,45 @@ "url": { "type": "string", "format": "uri" + }, + "method": { + "type": "string", + "enum": ["Get", "Head", "Post", "PostForm"], + "default": "Get" + }, + "body": { + "type": "string" + }, + "parameters": { + "type": "object", + "additionalProperties": { "type": "string"} + }, + "options": { + "$ref": "#/definitions/api-options" } } + }, + "required": ["name", "url"] + }, + "options": { + "$ref": "#/definitions/api-options" + } + }, + "required": ["requests"] + }, + "api-options": { + "type": "object", + "properties": { + "timeout": { + "type": "integer" + }, + "proxy": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "string" } } } diff --git a/src/pkg/domains/api/api.go b/src/pkg/domains/api/api.go index 14dce2b1..418fda8f 100644 --- a/src/pkg/domains/api/api.go +++ b/src/pkg/domains/api/api.go @@ -2,22 +2,44 @@ package api import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" + "net/url" + "time" "github.com/defenseunicorns/lula/src/types" ) -func MakeRequests(Requests []Request) (types.DomainResources, error) { +func (a ApiDomain) makeRequests(_ context.Context) (types.DomainResources, error) { collection := make(map[string]interface{}, 0) - for _, request := range Requests { - transport := &http.Transport{} - client := &http.Client{Transport: transport} + var defaultOpts *ApiOpts + if a.Spec.Options == nil { + defaultOpts = new(ApiOpts) + } else { + defaultOpts = a.Spec.Options + } + + // configure the default HTTP client using any top-level Options. Individual requests with overrides will get bespoke clients. + transport := &http.Transport{} + if defaultOpts.Proxy != "" { + proxy, err := url.Parse(a.Spec.Options.Proxy) + if err != nil { + return nil, fmt.Errorf("error parsing proxy url: %s", err) + } + transport.Proxy = http.ProxyURL(proxy) + } + + defaultClient := &http.Client{Transport: transport} + if defaultOpts.Timeout != 0 { + defaultClient.Timeout = time.Duration(defaultOpts.Timeout) * time.Second + } - resp, err := client.Get(request.URL) + for _, request := range a.Spec.Requests { + resp, err := defaultClient.Get(request.URL) if err != nil { return nil, err } diff --git a/src/pkg/domains/api/spec.go b/src/pkg/domains/api/spec.go new file mode 100644 index 00000000..50290bfc --- /dev/null +++ b/src/pkg/domains/api/spec.go @@ -0,0 +1,29 @@ +package api + +import ( + "errors" + "fmt" +) + +func validateSpec(spec *ApiSpec) error { + if spec == nil { + return errors.New("spec is required") + } + if len(spec.Requests) == 0 { + return errors.New("some requests must be specified") + } + for _, request := range spec.Requests { + if request.Name == "" { + return errors.New("request name cannot be empty") + } + if request.URL == "" { + return errors.New("request url cannot be empty") + } + if request.Method != "" { + if request.Method != "Get" && request.Method != "Head" && request.Method != "Post" && request.Method != "PostForm" { + return fmt.Errorf("unsupported method: %s", request.Method) + } + } + } + return nil +} diff --git a/src/pkg/domains/api/types.go b/src/pkg/domains/api/types.go index bc67c47d..efe0e516 100644 --- a/src/pkg/domains/api/types.go +++ b/src/pkg/domains/api/types.go @@ -2,7 +2,6 @@ package api import ( "context" - "fmt" "github.com/defenseunicorns/lula/src/types" ) @@ -15,20 +14,9 @@ type ApiDomain struct { func CreateApiDomain(spec *ApiSpec) (types.Domain, error) { // Check validity of spec - if spec == nil { - return nil, fmt.Errorf("spec is nil") - } - - if len(spec.Requests) == 0 { - return nil, fmt.Errorf("some requests must be specified") - } - for _, request := range spec.Requests { - if request.Name == "" { - return nil, fmt.Errorf("request name cannot be empty") - } - if request.URL == "" { - return nil, fmt.Errorf("request url cannot be empty") - } + err := validateSpec(spec) + if err != nil { + return nil, err } return ApiDomain{ @@ -36,8 +24,8 @@ func CreateApiDomain(spec *ApiSpec) (types.Domain, error) { }, nil } -func (a ApiDomain) GetResources(_ context.Context) (types.DomainResources, error) { - return MakeRequests(a.Spec.Requests) +func (a ApiDomain) GetResources(ctx context.Context) (types.DomainResources, error) { + return a.makeRequests(ctx) } func (a ApiDomain) IsExecutable() bool { @@ -48,10 +36,24 @@ func (a ApiDomain) IsExecutable() bool { // ApiSpec contains a list of API requests type ApiSpec struct { Requests []Request `mapstructure:"requests" json:"requests" yaml:"requests"` + // Opts will be applied to all requests, except those which have their own specified ApiOpts + Options *ApiOpts `mapstructure:"options" json:"options,omitempty" yaml:"options,omitempty"` } // Request is a single API request type Request struct { - Name string `json:"name" yaml:"name"` - URL string `json:"url" yaml:"url"` + Name string `json:"name" yaml:"name"` + URL string `json:"url" yaml:"url"` + Method string `json:"method,omitempty" yaml:"method,omitempty"` + Body string `json:"body,omitempty" yaml:"body,omitempty"` + Params map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty"` + // ApiOpts specific to this request + Options *ApiOpts `json:"options,omitempty" yaml:"options,omitempty"` +} + +type ApiOpts struct { + // Timeout in seconds + Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` + Headers []string `json:"headers,omitempty" yaml:"headers,omitempty"` } From 1083037208028ca03696d368c52ec16fff9f22b2 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 22 Oct 2024 09:06:57 -0400 Subject: [PATCH 2/2] updates --- src/pkg/domains/api/types.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pkg/domains/api/types.go b/src/pkg/domains/api/types.go index efe0e516..ca7fb2d3 100644 --- a/src/pkg/domains/api/types.go +++ b/src/pkg/domains/api/types.go @@ -42,18 +42,19 @@ type ApiSpec struct { // Request is a single API request type Request struct { - Name string `json:"name" yaml:"name"` - URL string `json:"url" yaml:"url"` - Method string `json:"method,omitempty" yaml:"method,omitempty"` - Body string `json:"body,omitempty" yaml:"body,omitempty"` - Params map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Name string `json:"name" yaml:"name"` + URL string `json:"url" yaml:"url"` + Method string `json:"method,omitempty" yaml:"method,omitempty"` + IsExecutable bool `json:"executable,omitempty" yaml:"executable,omitempty"` + Body string `json:"body,omitempty" yaml:"body,omitempty"` + Params map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty"` // ApiOpts specific to this request Options *ApiOpts `json:"options,omitempty" yaml:"options,omitempty"` } type ApiOpts struct { // Timeout in seconds - Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` Headers []string `json:"headers,omitempty" yaml:"headers,omitempty"` }