Imported and modified evcli's api client code

This commit is contained in:
Julien Dessaux 2023-06-18 17:41:38 +02:00
parent 64ac1f44e0
commit 6844355a92
Signed by: adyxax
GPG key ID: F92E51B86E07177E
11 changed files with 523 additions and 5 deletions

13
external/evcli/LICENSE vendored Normal file
View file

@ -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.

3
external/evcli/README.md vendored Normal file
View file

@ -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).

86
external/evcli/api.go vendored Normal file
View file

@ -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"`
}

332
external/evcli/client.go vendored Normal file
View file

@ -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)
}

6
external/evcli/config.go vendored Normal file
View file

@ -0,0 +1,6 @@
package evcli
type APIConfig struct {
Endpoint string `json:"endpoint,omitempty"`
Key string `json:"key,omitempty"`
}

14
external/evcli/http.go vendored Normal file
View file

@ -0,0 +1,14 @@
package evcli
import (
"net/http"
"time"
)
func NewHTTPClient() *http.Client {
c := &http.Client{
Timeout: 30 * time.Second,
}
return c
}

42
external/evcli/url.go vendored Normal file
View file

@ -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,
}
}

23
external/evcli/url_test.go vendored Normal file
View file

@ -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())
}

View file

@ -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"

View file

@ -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"

View file

@ -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