Skip to content

Commit

Permalink
Implement POSTing request bodies to the endpoint.
Browse files Browse the repository at this point in the history
This adds support to the APIClient generator to generate by posting a
body against an HTTP endpoint.
  • Loading branch information
bigkevmcd committed Mar 1, 2023
1 parent a39057a commit c5eb716
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 14 deletions.
7 changes: 6 additions & 1 deletion api/v1alpha1/gitopsset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type APIClientGenerator struct {
Interval metav1.Duration `json:"interval"`

// This is the API endpoint to use.
// +kubebuilder:validation:Pattern="^https://"
// +kubebuilder:validation:Pattern="^(http|https)://"
// +optional
Endpoint string `json:"endpoint,omitempty"`

Expand All @@ -99,6 +99,11 @@ type APIClientGenerator struct {
//
// +optional
HeadersRef *HeadersReference `json:"headersRef"`

// Body is set as the body in a POST request.
//
// If set, this will configure the Method to be POST automatically.
Body *apiextensionsv1.JSON `json:"body"`
}

// HeadersReference references either a Secret or ConfigMap to be used for
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

15 changes: 13 additions & 2 deletions config/crd/bases/templates.weave.works_gitopssets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ spec:
description: APIClientGenerator defines a generator that queries
an API endpoint and uses that to generate data.
properties:
body:
description: "Body is set as the body in a POST request.
\n If set, this will configure the Method to be POST automatically."
x-kubernetes-preserve-unknown-fields: true
endpoint:
description: This is the API endpoint to use.
pattern: ^https://
pattern: ^(http|https)://
type: string
headersRef:
description: "HeadersRef allows optional configuration of
Expand Down Expand Up @@ -100,6 +104,7 @@ spec:
- POST
type: string
required:
- body
- interval
type: object
cluster:
Expand Down Expand Up @@ -222,9 +227,14 @@ spec:
that queries an API endpoint and uses that to generate
data.
properties:
body:
description: "Body is set as the body in a POST
request. \n If set, this will configure the
Method to be POST automatically."
x-kubernetes-preserve-unknown-fields: true
endpoint:
description: This is the API endpoint to use.
pattern: ^https://
pattern: ^(http|https)://
type: string
headersRef:
description: "HeadersRef allows optional configuration
Expand Down Expand Up @@ -267,6 +277,7 @@ spec:
- POST
type: string
required:
- body
- interval
type: object
cluster:
Expand Down
17 changes: 16 additions & 1 deletion controllers/templates/generators/apiclient/api_client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apiclient

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -97,11 +98,25 @@ func (g *APIClientGenerator) Interval(sg *templatesv1.GitOpsSetGenerator) time.D
}

func (g *APIClientGenerator) createRequest(ctx context.Context, ac *templatesv1.APIClientGenerator, namespace string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, ac.Method, ac.Endpoint, nil)
method := ac.Method
if ac.Body != nil {
method = http.MethodPost
}

var body io.Reader
if ac.Body != nil {
body = bytes.NewReader(ac.Body.Raw)
}

req, err := http.NewRequestWithContext(ctx, method, ac.Endpoint, body)
if err != nil {
return nil, err
}

if body != nil {
req.Header.Set("Content-Type", "application/json")
}

if ac.HeadersRef != nil {
if ac.HeadersRef.Kind == "Secret" {
return req, addHeadersFromSecretToRequest(ctx, g.Client, req, client.ObjectKey{Name: ac.HeadersRef.Name, Namespace: namespace})
Expand Down
49 changes: 46 additions & 3 deletions controllers/templates/generators/apiclient/api_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ import (

"github.com/go-logr/logr"
"github.com/google/go-cmp/cmp"
templatesv1 "github.com/weaveworks/gitopssets-controller/api/v1alpha1"
"github.com/weaveworks/gitopssets-controller/controllers/templates/generators"
"github.com/weaveworks/gitopssets-controller/test"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

templatesv1 "github.com/weaveworks/gitopssets-controller/api/v1alpha1"
"github.com/weaveworks/gitopssets-controller/controllers/templates/generators"
"github.com/weaveworks/gitopssets-controller/test"
)

var _ generators.Generator = (*APIClientGenerator)(nil)
Expand Down Expand Up @@ -85,6 +87,20 @@ func TestGenerate(t *testing.T) {
},
},
},
{
name: "simple API endpoint with POST body request",
apiClient: &templatesv1.APIClientGenerator{
Endpoint: ts.URL + "/api/post-body",
Method: http.MethodPost,
Body: &apiextensionsv1.JSON{Raw: []byte(`{"user":"demo","groups":["group1"]}`)},
},
want: []map[string]any{
{
"name": "demo",
"groups": []any{"group1"},
},
},
},
{
name: "api endpoint returning map with JSONPath",
apiClient: &templatesv1.APIClientGenerator{
Expand Down Expand Up @@ -370,6 +386,33 @@ func newTestMux(t *testing.T) *http.ServeMux {
writeResponse(w)
})

mux.HandleFunc("/api/post-body", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "unsupported content type", http.StatusUnsupportedMediaType)
return
}
var body map[string]any
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&body); err != nil {
http.Error(w, "invalid json "+err.Error(), http.StatusBadRequest)
return
}

enc := json.NewEncoder(w)
if err := enc.Encode([]map[string]any{
{
"name": body["user"],
"groups": body["groups"],
},
}); err != nil {
t.Fatal(err)
}
})

mux.HandleFunc("/api/non-array", func(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
if err := enc.Encode(map[string]any{
Expand Down
45 changes: 38 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,11 @@ In this case, six different `ConfigMaps` are generated, three for the "dev-team"
## Generators

We currently provide these generators:

- list
- pullRequests
- gitRepository
- matrix
- apiClient
- [list](#list-generator)
- [pullRequests](#pullrequests-generator)
- [gitRepository](#gitrepository-generator)
- [matrix](#matrix-generator)
- [apiClient](#apiclient-generator)

### List generator

Expand Down Expand Up @@ -537,7 +536,7 @@ Not all APIs return an array of JSON objects, sometimes it's nested within a res
}
```
You can use JSONPath to extract the fields from this data...
```
```yaml
apiVersion: templates.weave.works/v1alpha1
kind: GitOpsSet
metadata:
Expand All @@ -557,6 +556,38 @@ spec:
```
This will generate three maps for templates, with just the _env_ and _team_ keys.

#### APIClient POST body

Another piece of functionality in the APIClient generator is the ability to POST
JSON to the API.
```yaml
apiVersion: templates.weave.works/v1alpha1
kind: GitOpsSet
metadata:
labels:
app.kubernetes.io/name: gitopsset
app.kubernetes.io/instance: gitopsset-sample
app.kubernetes.io/part-of: gitopssets-controller
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/created-by: gitopssets-controller
name: api-client-sample
spec:
generators:
- apiClient:
interval: 5m
endpoint: https://api.example.com/demo
body:
name: "testing"
value: "testing2"
```
This will send a request body as JSON (Content-Type "application/json") to the
server and interpret the result.

The JSON body sent will look like this:
```json
{"name":"testing","value":"testing2"}
```

## Templating functions

Currently, the [Sprig](http://masterminds.github.io/sprig/) functions are available in the templating, with some functions removed[^sprig] for security reasons.
Expand Down

0 comments on commit c5eb716

Please sign in to comment.