aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Dessaux2023-06-18 17:41:38 +0200
committerJulien Dessaux2023-06-18 17:43:53 +0200
commit6844355a928b60ffb1006e6b1d9f8af7b44647d5 (patch)
tree57355ad7bf98b870cf96ceec2a2eae64b94d3276
parentImplemented project resource (diff)
downloadterraform-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/LICENSE13
-rw-r--r--external/evcli/README.md3
-rw-r--r--external/evcli/api.go86
-rw-r--r--external/evcli/client.go332
-rw-r--r--external/evcli/config.go6
-rw-r--r--external/evcli/http.go14
-rw-r--r--external/evcli/url.go42
-rw-r--r--external/evcli/url_test.go23
-rw-r--r--internal/provider/project_resource.go2
-rw-r--r--internal/provider/projects_data_source.go2
-rw-r--r--internal/provider/provider.go5
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