diff options
author | Julien Dessaux | 2023-06-18 17:41:38 +0200 |
---|---|---|
committer | Julien Dessaux | 2023-06-18 17:43:53 +0200 |
commit | 6844355a928b60ffb1006e6b1d9f8af7b44647d5 (patch) | |
tree | 57355ad7bf98b870cf96ceec2a2eae64b94d3276 | |
parent | Implemented project resource (diff) | |
download | terraform-provider-eventline-6844355a928b60ffb1006e6b1d9f8af7b44647d5.tar.gz terraform-provider-eventline-6844355a928b60ffb1006e6b1d9f8af7b44647d5.tar.bz2 terraform-provider-eventline-6844355a928b60ffb1006e6b1d9f8af7b44647d5.zip |
Imported and modified evcli's api client code
-rw-r--r-- | external/evcli/LICENSE | 13 | ||||
-rw-r--r-- | external/evcli/README.md | 3 | ||||
-rw-r--r-- | external/evcli/api.go | 86 | ||||
-rw-r--r-- | external/evcli/client.go | 332 | ||||
-rw-r--r-- | external/evcli/config.go | 6 | ||||
-rw-r--r-- | external/evcli/http.go | 14 | ||||
-rw-r--r-- | external/evcli/url.go | 42 | ||||
-rw-r--r-- | external/evcli/url_test.go | 23 | ||||
-rw-r--r-- | internal/provider/project_resource.go | 2 | ||||
-rw-r--r-- | internal/provider/projects_data_source.go | 2 | ||||
-rw-r--r-- | internal/provider/provider.go | 5 |
11 files changed, 523 insertions, 5 deletions
diff --git a/external/evcli/LICENSE b/external/evcli/LICENSE new file mode 100644 index 0000000..eec6477 --- /dev/null +++ b/external/evcli/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2022 Exograd SAS. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/external/evcli/README.md b/external/evcli/README.md new file mode 100644 index 0000000..91e7820 --- /dev/null +++ b/external/evcli/README.md @@ -0,0 +1,3 @@ +# evcli + +The code in this directory comes from [evcli](https://github.com/exograd/eventline/tree/master/cmd/evcli). It is the property of [Exograd](https://www.exograd.com/) under the [ISC License](https://github.com/exograd/eventline/blob/master/LICENSE). diff --git a/external/evcli/api.go b/external/evcli/api.go new file mode 100644 index 0000000..40fe38c --- /dev/null +++ b/external/evcli/api.go @@ -0,0 +1,86 @@ +package evcli + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/exograd/eventline/pkg/eventline" + "github.com/exograd/go-daemon/check" +) + +type APIError struct { + Message string `json:"error"` + Code string `json:"code,omitempty"` + RawData json.RawMessage `json:"data,omitempty"` + Data interface{} `json:"-"` +} + +type InvalidRequestBodyError struct { + ValidationErrors check.ValidationErrors `json:"validation_errors"` +} + +func (err APIError) Error() string { + return err.Message +} + +func (err *APIError) UnmarshalJSON(data []byte) error { + type APIError2 APIError + + err2 := APIError2(*err) + if jsonErr := json.Unmarshal(data, &err2); jsonErr != nil { + return jsonErr + } + + switch err2.Code { + case "invalid_request_body": + var errData InvalidRequestBodyError + + if err2.RawData != nil { + if err := json.Unmarshal(err2.RawData, &errData); err != nil { + return fmt.Errorf("invalid jsv errors: %w", err) + } + + err2.Data = &errData + } + } + + *err = APIError(err2) + return nil +} + +func IsInvalidRequestBodyError(err error) (bool, check.ValidationErrors) { + var apiError *APIError + + if !errors.As(err, &apiError) { + return false, nil + } + + requestBodyErr, ok := apiError.Data.(*InvalidRequestBodyError) + if !ok { + return false, nil + } + + return true, requestBodyErr.ValidationErrors +} + +type ProjectPage struct { + Elements eventline.Projects `json:"elements"` + Previous *eventline.Cursor `json:"previous,omitempty"` + Next *eventline.Cursor `json:"next,omitempty"` +} + +type Parameter struct { + Name string `json:"name"` + Type string `json:"type"` + Default interface{} `json:"default"` + Description string `json:"description"` +} + +type Parameters []*Parameter + +type JobPage struct { + Elements eventline.Jobs `json:"elements"` + Previous *eventline.Cursor `json:"previous,omitempty"` + Next *eventline.Cursor `json:"next,omitempty"` +} diff --git a/external/evcli/client.go b/external/evcli/client.go new file mode 100644 index 0000000..8127dff --- /dev/null +++ b/external/evcli/client.go @@ -0,0 +1,332 @@ +package evcli + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + + "github.com/exograd/eventline/pkg/eventline" +) + +type Client struct { + APIKey string + ProjectId *eventline.Id + + httpClient *http.Client + + baseURI *url.URL +} + +func NewClient(config *APIConfig) (*Client, error) { + baseURI, err := url.Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("invalid api endpoint: %w", err) + } + + client := &Client{ + APIKey: config.Key, + baseURI: baseURI, + httpClient: NewHTTPClient(), + } + + if err != nil { + } + + return client, nil +} + +func (c *Client) SendRequest(method string, relURI *url.URL, body, dest interface{}) error { + uri := c.baseURI.ResolveReference(relURI) + + var bodyReader io.Reader + if body == nil { + bodyReader = nil + } else if br, ok := body.(io.Reader); ok { + bodyReader = br + } else { + bodyData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("cannot encode body: %w", err) + } + + bodyReader = bytes.NewReader(bodyData) + } + + req, err := http.NewRequest(method, uri.String(), bodyReader) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + + if c.APIKey != "" { + req.Header.Set("Authorization", "Bearer "+c.APIKey) + } + + if c.ProjectId != nil { + req.Header.Set("X-Eventline-Project-Id", c.ProjectId.String()) + } + + res, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("cannot send request: %w", err) + } + defer res.Body.Close() + + resBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("cannot read response body: %w", err) + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + var apiErr APIError + + err := json.Unmarshal(resBody, &apiErr) + if err == nil { + return &apiErr + } + + return fmt.Errorf("request failed with status %d: %s", + res.StatusCode, string(resBody)) + } + + if dest != nil { + if dataPtr, ok := dest.(*[]byte); ok { + *dataPtr = resBody + } else { + if len(resBody) == 0 { + return fmt.Errorf("empty response body") + } + + if err := json.Unmarshal(resBody, dest); err != nil { + return fmt.Errorf("cannot decode response body: %w", err) + } + } + } + + return err +} + +func (c *Client) FetchProjects() (eventline.Projects, error) { + var projects eventline.Projects + + cursor := eventline.Cursor{Size: 1} + + for { + var page ProjectPage + + uri := NewURL("projects") + uri.RawQuery = cursor.Query().Encode() + + err := c.SendRequest("GET", uri, nil, &page) + if err != nil { + return nil, err + } + + projects = append(projects, page.Elements...) + + if page.Next == nil { + break + } + + cursor = *page.Next + } + + return projects, nil +} + +func (c *Client) FetchProjectById(id eventline.Id) (*eventline.Project, error) { + uri := NewURL("projects", "id", id.String()) + + var project eventline.Project + + err := c.SendRequest("GET", uri, nil, &project) + if err != nil { + return nil, err + } + + return &project, nil +} + +func (c *Client) FetchProjectByName(name string) (*eventline.Project, error) { + uri := NewURL("projects", "name", name) + + var project eventline.Project + + err := c.SendRequest("GET", uri, nil, &project) + if err != nil { + return nil, err + } + + return &project, nil +} + +func (c *Client) CreateProject(project *eventline.Project) error { + uri := NewURL("projects") + + return c.SendRequest("POST", uri, project, project) +} + +func (c *Client) DeleteProject(id eventline.Id) error { + uri := NewURL("projects", "id", id.String()) + + return c.SendRequest("DELETE", uri, nil, nil) +} + +func (c *Client) UpdateProject(project *eventline.Project) error { + uri := NewURL("projects", "id", project.Id.String()) + + return c.SendRequest("PUT", uri, project, nil) +} + +func (c *Client) ReplayEvent(id string) (*eventline.Event, error) { + var event eventline.Event + + uri := NewURL("events", "id", id, "replay") + + err := c.SendRequest("POST", uri, nil, &event) + if err != nil { + return nil, err + } + + return &event, nil +} + +func (c *Client) FetchJobByName(name string) (*eventline.Job, error) { + uri := NewURL("jobs", "name", name) + + var job eventline.Job + + err := c.SendRequest("GET", uri, nil, &job) + if err != nil { + return nil, err + } + + return &job, nil +} + +func (c *Client) FetchJobs() (eventline.Jobs, error) { + var jobs eventline.Jobs + + cursor := eventline.Cursor{Size: 20} + + for { + var page JobPage + + uri := NewURL("jobs") + uri.RawQuery = cursor.Query().Encode() + + err := c.SendRequest("GET", uri, nil, &page) + if err != nil { + return nil, err + } + + jobs = append(jobs, page.Elements...) + + if page.Next == nil { + break + } + + cursor = *page.Next + } + + return jobs, nil +} + +func (c *Client) DeployJob(spec *eventline.JobSpec, dryRun bool) (*eventline.Job, error) { + uri := NewURL("jobs", "name", spec.Name) + + query := url.Values{} + if dryRun { + query.Add("dry-run", "") + } + uri.RawQuery = query.Encode() + + if dryRun { + if err := c.SendRequest("PUT", uri, spec, nil); err != nil { + return nil, err + } + + return nil, nil + } else { + var job eventline.Job + + if err := c.SendRequest("PUT", uri, spec, &job); err != nil { + return nil, err + } + + return &job, nil + } + +} + +func (c *Client) DeployJobs(specs []*eventline.JobSpec, dryRun bool) ([]*eventline.Job, error) { + uri := NewURL("jobs") + + query := url.Values{} + if dryRun { + query.Add("dry-run", "") + } + uri.RawQuery = query.Encode() + + if dryRun { + if err := c.SendRequest("PUT", uri, specs, nil); err != nil { + return nil, err + } + + return nil, nil + } else { + var jobs []*eventline.Job + + if err := c.SendRequest("PUT", uri, specs, &jobs); err != nil { + return nil, err + } + + return jobs, nil + } +} + +func (c *Client) DeleteJob(id string) error { + uri := NewURL("jobs", "id", id) + + return c.SendRequest("DELETE", uri, nil, nil) +} + +func (c *Client) ExecuteJob(id string, input *eventline.JobExecutionInput) (*eventline.JobExecution, error) { + uri := NewURL("jobs", "id", id, "execute") + + var jobExecution eventline.JobExecution + + if err := c.SendRequest("POST", uri, input, &jobExecution); err != nil { + return nil, err + } + + return &jobExecution, nil +} + +func (c *Client) FetchJobExecution(id eventline.Id) (*eventline.JobExecution, error) { + uri := NewURL("job_executions", "id", id.String()) + + var je eventline.JobExecution + + err := c.SendRequest("GET", uri, nil, &je) + if err != nil { + return nil, err + } + + return &je, nil +} + +func (c *Client) AbortJobExecution(id eventline.Id) error { + uri := NewURL("job_executions", "id", id.String(), "abort") + + return c.SendRequest("POST", uri, nil, nil) +} + +func (c *Client) RestartJobExecution(id eventline.Id) error { + uri := NewURL("job_executions", "id", id.String(), "restart") + + return c.SendRequest("POST", uri, nil, nil) +} diff --git a/external/evcli/config.go b/external/evcli/config.go new file mode 100644 index 0000000..df20c45 --- /dev/null +++ b/external/evcli/config.go @@ -0,0 +1,6 @@ +package evcli + +type APIConfig struct { + Endpoint string `json:"endpoint,omitempty"` + Key string `json:"key,omitempty"` +} diff --git a/external/evcli/http.go b/external/evcli/http.go new file mode 100644 index 0000000..f4aae1c --- /dev/null +++ b/external/evcli/http.go @@ -0,0 +1,14 @@ +package evcli + +import ( + "net/http" + "time" +) + +func NewHTTPClient() *http.Client { + c := &http.Client{ + Timeout: 30 * time.Second, + } + + return c +} diff --git a/external/evcli/url.go b/external/evcli/url.go new file mode 100644 index 0000000..e1e99a8 --- /dev/null +++ b/external/evcli/url.go @@ -0,0 +1,42 @@ +package evcli + +import ( + "bytes" + "net/url" +) + +// The url package has an extremely confusing interface. One could believe +// setting RawPath is enough for it to be used during encoding, but this is +// not the case. Instead, *both* must be set, and (*url.URL).EscapedPath will +// check that RawPath is a valid encoding of RawPath. If this is not the case, +// RawPath will be ignored. +// +// In most cases, the problem is not apparent. But if the path contains +// segments with slash or space characters, it is almost guaranteed to misuse +// Path and RawPath and double-encode these characters. +// +// The problem was signaled years ago on +// https://github.com/golang/go/issues/17340 but was of course ignored and +// buried. +// +// As usual with standard library issues, the only thing we can do is add +// utils to work around it. + +func NewURL(pathSegments ...string) *url.URL { + var buf bytes.Buffer + buf.WriteByte('/') + for i, s := range pathSegments { + if i > 0 { + buf.WriteByte('/') + } + buf.WriteString(url.PathEscape(s)) + } + rawPath := buf.String() + + path, _ := url.PathUnescape(rawPath) + + return &url.URL{ + Path: path, + RawPath: rawPath, + } +} diff --git a/external/evcli/url_test.go b/external/evcli/url_test.go new file mode 100644 index 0000000..aa13281 --- /dev/null +++ b/external/evcli/url_test.go @@ -0,0 +1,23 @@ +package evcli + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewURL(t *testing.T) { + assert := assert.New(t) + + assert.Equal("/", + NewURL().String()) + + assert.Equal("/a", + NewURL("a").String()) + + assert.Equal("/a/bcd/ef", + NewURL("a", "bcd", "ef").String()) + + assert.Equal("/a/b%20c/e%2Ff", + NewURL("a", "b c", "e/f").String()) +} diff --git a/internal/provider/project_resource.go b/internal/provider/project_resource.go index 9e9d9e2..541f8b1 100644 --- a/internal/provider/project_resource.go +++ b/internal/provider/project_resource.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "git.adyxax.org/adyxax/terraform-eventline/internal/evcli" + "git.adyxax.org/adyxax/terraform-eventline/external/evcli" "github.com/exograd/eventline/pkg/eventline" "github.com/exograd/go-daemon/ksuid" "github.com/hashicorp/terraform-plugin-framework/path" diff --git a/internal/provider/projects_data_source.go b/internal/provider/projects_data_source.go index e0b5b8b..8ae8862 100644 --- a/internal/provider/projects_data_source.go +++ b/internal/provider/projects_data_source.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "git.adyxax.org/adyxax/terraform-eventline/internal/evcli" + "git.adyxax.org/adyxax/terraform-eventline/external/evcli" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 3ae769e..635580f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "git.adyxax.org/adyxax/terraform-eventline/internal/evcli" + "git.adyxax.org/adyxax/terraform-eventline/external/evcli" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -57,13 +57,12 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, if resp.Diagnostics.HasError() { return } - config := evcli.Config{API: evcli.APIConfig{Endpoint: data.Endpoint.ValueString()}} + config := evcli.APIConfig{Endpoint: data.Endpoint.ValueString(), Key: data.ApiKey.ValueString()} client, err := evcli.NewClient(&config) if err != nil { resp.Diagnostics.AddError("new api client", fmt.Sprintf("Unable to instanciate eventline api client, got error: %s", err)) return } - client.APIKey = data.ApiKey.ValueString() resp.DataSourceData = client resp.ResourceData = client |