Skip to content

Commit

Permalink
Allow REST data sources to form bodies from input
Browse files Browse the repository at this point in the history
This adds a new option to the REST data source driver to form a body
from input. This will be marshaled as JSON or interpreted as a string,
and will be used as the message body.

This allows us to dynamically form the body in order to better support
POST-based implementations like OSV.

the new field is called `body_from_field`. The field *must* exist in the
input parameters and must be marked as required or have an appropriate
default. The field must also be a top-level parameter.

Here's an example of a data source that uses it:

```yaml

---
version: v1
type: data-source
name: osv
context: {}
rest:
  def:
    query:
      # Supports templating based on RFC6570
      endpoint: 'https://api.osv.dev/v1/query'
      parse: json
      method: POST
      body_obj_from_field: query
      input_schema:
        type: object
        properties:
          query:
            type: object
            properties:
              version:
                type: string
              package:
                type: object
                properties:
                  ecosystem:
                    type: string
                    description: The ecosystem the dependency belongs to
                  name:
                    type: string
                    description: The name of the dependency
        required:
          - query
```

Note in this case we do validation of all parameters that can be passed
to the body.

usage within a rego evaluation policy would look as follows:

```
...

          reqparams := {
              "query": {
                "version": version,
                "package": {
                  "name": name,
                  "ecosystem": "PyPI"
                }
              }
            }

          out := minder.datasource.osv.query(reqparams)
	  ...
```

Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
  • Loading branch information
JAORMX committed Dec 4, 2024
1 parent 19a4d9f commit d421bb6
Show file tree
Hide file tree
Showing 7 changed files with 1,039 additions and 872 deletions.
1 change: 1 addition & 0 deletions docs/docs/ref/proto.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 59 additions & 16 deletions internal/datasources/rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ type restHandler struct {
inputSchema *jsonschema.Schema
endpointTmpl string
method string
body string
headers map[string]string
parse string
// contains the request body or the key
body string
bodyFromInput bool
headers map[string]string
parse string
// TODO implement fallback
// TODO implement auth
}
Expand All @@ -52,13 +54,16 @@ func newHandlerFromDef(def *minderv1.RestDataSource_Def) (*restHandler, error) {
return nil, err
}

bodyFromInput, body := parseRequestBodyConfig(def)

return &restHandler{
rawInputSchema: def.GetInputSchema(),
inputSchema: schema,
endpointTmpl: def.GetEndpoint(),
method: util.HttpMethodFromString(def.GetMethod(), http.MethodGet),
headers: def.GetHeaders(),
body: parseRequestBodyConfig(def),
body: body,
bodyFromInput: bodyFromInput,
parse: def.GetParse(),
}, nil
}
Expand Down Expand Up @@ -118,9 +123,9 @@ func (h *restHandler) Call(_ context.Context, args any) (any, error) {
Timeout: 5 * time.Second,
}

var b io.Reader
if h.body != "" {
b = strings.NewReader(h.body)
b, err := h.getBody(argsMap)
if err != nil {
return nil, err
}

req, err := http.NewRequest(h.method, expandedEndpoint, b)
Expand Down Expand Up @@ -149,6 +154,44 @@ func (h *restHandler) Call(_ context.Context, args any) (any, error) {
return buildRestOutput(resp.StatusCode, bout), nil
}

func (h *restHandler) getBody(args map[string]any) (io.Reader, error) {
if h.bodyFromInput {
return h.getBodyFromInput(args)
}

if h.body == "" {
return nil, nil
}

return strings.NewReader(h.body), nil
}

func (h *restHandler) getBodyFromInput(args map[string]any) (io.Reader, error) {
if h.body == "" {
return nil, errors.New("body key is empty")
}

body, ok := args[h.body]
if !ok {
return nil, fmt.Errorf("body key %q not found in args", h.body)
}

switch outb := body.(type) {
case string:
return strings.NewReader(outb), nil
case map[string]any:
// stringify the object
obj, err := json.Marshal(outb)
if err != nil {
return nil, fmt.Errorf("cannot marshal body object: %w", err)
}

return strings.NewReader(string(obj)), nil
default:
return nil, fmt.Errorf("body key %q is not a string or object", h.body)
}
}

func (h *restHandler) parseResponseBody(body io.Reader) (any, error) {
var data any

Expand Down Expand Up @@ -178,26 +221,26 @@ func (h *restHandler) parseResponseBody(body io.Reader) (any, error) {
return data, nil
}

// body may be unset, in which case it is nil
// or it may be an object or a string. We are using
// a oneof in the protobuf definition to represent this.
func parseRequestBodyConfig(def *minderv1.RestDataSource_Def) string {
func parseRequestBodyConfig(def *minderv1.RestDataSource_Def) (bool, string) {
defBody := def.GetBody()
if defBody == nil {
return ""
return false, ""
}

if def.GetBodyobj() != nil {
switch defBody.(type) {
case *minderv1.RestDataSource_Def_Bodyobj:
// stringify the object
obj, err := json.Marshal(def.GetBodyobj())
if err != nil {
return ""
return false, ""
}

return string(obj)
return false, string(obj)
case *minderv1.RestDataSource_Def_BodyFromField:
return true, def.GetBodyFromField()
}

return def.GetBodystr()
return false, def.GetBodystr()
}

func buildRestOutput(statusCode int, body any) any {
Expand Down
24 changes: 19 additions & 5 deletions internal/datasources/rest/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ func Test_parseRequestBodyConfig(t *testing.T) {
def *minderv1.RestDataSource_Def
}
tests := []struct {
name string
args args
want string
name string
args args
want string
wantFromInput bool
}{
{
name: "Nil body",
Expand Down Expand Up @@ -124,13 +125,26 @@ func Test_parseRequestBodyConfig(t *testing.T) {
},
want: `{"key":"value"}`,
},
{
name: "Body from input",
args: args{
def: &minderv1.RestDataSource_Def{
Body: &minderv1.RestDataSource_Def_BodyFromField{
BodyFromField: "key",
},
},
},
want: "key",
wantFromInput: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := parseRequestBodyConfig(tt.args.def)
assert.Equal(t, tt.want, got)
gotFromInput, gotStr := parseRequestBodyConfig(tt.args.def)
assert.Equal(t, tt.want, gotStr)
assert.Equal(t, tt.wantFromInput, gotFromInput)
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/openapi/minder/v1/minder.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d421bb6

Please sign in to comment.