From 11a55e27d0c15277ff20aa8f181f3fad826bc5aa Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Wed, 10 Apr 2024 12:38:12 -0700 Subject: [PATCH] add http resource create method --- internal/resource/http.go | 76 +++++++++++++--- internal/resource/http_test.go | 88 +++++++++++++++++-- internal/resource/schemas/document.jsonschema | 1 + .../schemas/http-declaration.jsonschema | 17 ++++ internal/resource/schemas/http.jsonschema | 31 +++++++ 5 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 internal/resource/schemas/http-declaration.jsonschema create mode 100644 internal/resource/schemas/http.jsonschema diff --git a/internal/resource/http.go b/internal/resource/http.go index 45bf12f..8870f1b 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -11,6 +11,9 @@ _ "errors" "net/url" "net/http" _ "os" + "encoding/json" + "strings" + "log/slog" ) func init() { @@ -20,19 +23,26 @@ func init() { func HTTPFactory(u *url.URL) Resource { h := NewHTTP() + h.Endpoint = u.String() return h } +type HTTPHeader struct { + Name string `yaml:"name" json:"name"` + Value string `yaml:"value" json"value"` +} + // Manage the state of an HTTP endpoint type HTTP struct { + client *http.Client `yaml:"-" json:"-"` Endpoint string `yaml:"endpoint" json:"endpoint"` - + Headers []HTTPHeader `yaml:"headers,omitempty", json:"headers,omitempty"` Body string `yaml:"body,omitempty" json:"body,omitempty"` State string `yaml:"state" json:"state"` } func NewHTTP() *HTTP { - return &HTTP{} + return &HTTP{ client: &http.Client{} } } func (h *HTTP) URI() string { @@ -47,36 +57,77 @@ func (h *HTTP) SetURI(uri string) error { return nil } +func (h *HTTP) JSON() ([]byte, error) { + return json.Marshal(h) +} + func (h *HTTP) Validate() error { - return fmt.Errorf("failed") + s := NewSchema(h.Type()) + jsonDoc, jsonErr := h.JSON() + if jsonErr == nil { + return s.Validate(string(jsonDoc)) + } + return jsonErr } func (h *HTTP) Apply() error { - switch h.State { case "absent": case "present": } - - return nil + _,e := h.Read(context.Background()) + if e == nil { + h.State = "present" + } + return e } func (h *HTTP) Load(r io.Reader) error { - c := NewYAMLDecoder(r) - return c.Decode(h) + c := NewYAMLDecoder(r) + return c.Decode(h) } func (h *HTTP) LoadDecl(yamlResourceDeclaration string) error { - c := NewYAMLStringDecoder(yamlResourceDeclaration) - return c.Decode(h) + slog.Info("LoadDecl()", "yaml", yamlResourceDeclaration) + c := NewYAMLStringDecoder(yamlResourceDeclaration) + return c.Decode(h) } func (h *HTTP) ResolveId(ctx context.Context) string { return h.Endpoint } +func (h *HTTP) Create() error { + body := strings.NewReader(h.Body) + req, reqErr := http.NewRequest("POST", h.Endpoint, body) + if reqErr != nil { + return reqErr + } + for _,header := range h.Headers { + req.Header.Add(header.Name, header.Value) + } + resp, err := h.client.Do(req) + defer resp.Body.Close() + if err != nil { + return err + } + return err +} + func (h *HTTP) Read(ctx context.Context) ([]byte, error) { - resp, err := http.Get(h.Endpoint) + req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, nil) + if reqErr != nil { + return nil, reqErr + } + slog.Info("HTTP.Read() ", "request", req, "err", reqErr) + + if len(h.Headers) > 0 { + for _,header := range h.Headers { + req.Header.Add(header.Name, header.Value) + } + } + + resp, err := h.client.Do(req) if err != nil { return nil, err } @@ -90,6 +141,5 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) { } func (h *HTTP) Type() string { - u, _ := url.Parse(h.Endpoint) - return u.Scheme + return "http" } diff --git a/internal/resource/http_test.go b/internal/resource/http_test.go index 2bc38da..a555393 100644 --- a/internal/resource/http_test.go +++ b/internal/resource/http_test.go @@ -3,23 +3,97 @@ package resource import ( - _ "context" + "context" _ "encoding/json" - _ "fmt" + "fmt" "github.com/stretchr/testify/assert" _ "gopkg.in/yaml.v3" - _ "io" + "io" _ "log" - _ "net/http" - _ "net/http/httptest" + "net/http" + "net/http/httptest" _ "net/url" _ "os" _ "path/filepath" _ "strings" "testing" + "regexp" ) func TestNewHTTPResource(t *testing.T) { - f := NewHTTP() - assert.NotNil(t, f) + h := NewHTTP() + assert.NotNil(t, h) +} + +func TestHTTPDecode(t *testing.T) { + h := NewHTTP() + assert.NotNil(t, h) + decl:=` +endpoint: "https://example.foo" +body: |- + test body +` + + assert.Nil(t, h.LoadDecl(decl)) + assert.Equal(t, "test body", h.Body) +} + +func TestHTTPRead(t *testing.T) { + h := NewHTTP() + assert.NotNil(t, h) + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.URL.String(), "/resource/user/foo") + rw.Write([]byte(` +type: "user" +attributes: + name: "foo" + gecos: "foo user" +`)) + })) + defer server.Close() + + decl := fmt.Sprintf(` +endpoint: "%s/resource/user/foo" +`, server.URL) + + assert.Nil(t, h.LoadDecl(decl)) + _,e := h.Read(context.Background()) + assert.Nil(t, e) + assert.Greater(t, len(h.Body), 0) + assert.Nil(t, h.Validate()) +} + +func TestHTTPCreate(t *testing.T) { + userdecl := ` +type: "user" +attributes: + name: "foo" + gecos: "foo user" +` + + h := NewHTTP() + assert.NotNil(t, h) + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + assert.Equal(t, req.URL.String(), "/resource/user") + body, err := io.ReadAll(req.Body) + assert.Nil(t, err) + assert.Equal(t, userdecl, string(body)) + })) + defer server.Close() + + re := regexp.MustCompile(`(?m)^(.*)$`) + decl := fmt.Sprintf(` +endpoint: "%s/resource/user" +headers: +- name: "content-type" + value: "application/yaml" +body: | +%s +`, server.URL, re.ReplaceAllString(userdecl, " $1")) + assert.Nil(t, h.LoadDecl(decl)) + assert.Greater(t, len(h.Body), 0) + e := h.Create() + assert.Nil(t, e) } diff --git a/internal/resource/schemas/document.jsonschema b/internal/resource/schemas/document.jsonschema index eb25e40..c724d31 100644 --- a/internal/resource/schemas/document.jsonschema +++ b/internal/resource/schemas/document.jsonschema @@ -12,6 +12,7 @@ "oneOf": [ { "$ref": "package-declaration.jsonschema" }, { "$ref": "file-declaration.jsonschema" }, + { "$ref": "http-declaration.jsonschema" }, { "$ref": "user-declaration.jsonschema" }, { "$ref": "exec-declaration.jsonschema" }, { "$ref": "network_route-declaration.jsonschema" } diff --git a/internal/resource/schemas/http-declaration.jsonschema b/internal/resource/schemas/http-declaration.jsonschema new file mode 100644 index 0000000..6888785 --- /dev/null +++ b/internal/resource/schemas/http-declaration.jsonschema @@ -0,0 +1,17 @@ +{ + "$id": "http-declaration.jsonschema", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "http-declaration", + "type": "object", + "required": [ "type", "attributes" ], + "properties": { + "type": { + "type": "string", + "description": "Resource type name.", + "enum": [ "http" ] + }, + "attributes": { + "$ref": "http.jsonschema" + } + } +} diff --git a/internal/resource/schemas/http.jsonschema b/internal/resource/schemas/http.jsonschema new file mode 100644 index 0000000..4f6c0c6 --- /dev/null +++ b/internal/resource/schemas/http.jsonschema @@ -0,0 +1,31 @@ +{ + "$id": "http.jsonschema", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "http", + "type": "object", + "required": [ "endpoint" ], + "properties": { + "endpoint": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "HTTP header name" + }, + "value": { + "type": "string", + "description": "HTTP header value" + } + } + } + }, + "body": { + "type": "string" + } + } +}