From 415afccc4cfdd8114a3774cc36c43037cf3c0205 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 15:54:34 +0800 Subject: [PATCH 1/6] new function CreateRunTrigger --- lib/run.go | 31 +++++++++++++++++++++++++++++++ lib/run_test.go | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/lib/run.go b/lib/run.go index 3fd485d..9cbd1a8 100644 --- a/lib/run.go +++ b/lib/run.go @@ -38,3 +38,34 @@ func buildRunPayload(message, workspaceID string) string { return data.String() } + +type RunTriggerConfig struct { + WorkspaceID string + SourceWorkspaceID string +} + +func CreateRunTrigger(config RunTriggerConfig) error { + u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") + payload := buildRunTriggerPayload(config.SourceWorkspaceID) + _ = callAPI(http.MethodPost, u.String(), payload, nil) + return nil +} + +func buildRunTriggerPayload(sourceWorkspaceID string) string { + data := gabs.New() + + _, err := data.Object("data") + if err != nil { + return "error" + } + + if _, err = data.SetP(sourceWorkspaceID, "data.relationships.sourceable.data.id"); err != nil { + return "unable to process attribute for update:" + err.Error() + } + + if _, err = data.SetP("workspaces", "data.relationships.sourceable.data.type"); err != nil { + return "unable to process attribute for update:" + err.Error() + } + + return data.String() +} diff --git a/lib/run_test.go b/lib/run_test.go index 43be5ec..4ce7a3a 100644 --- a/lib/run_test.go +++ b/lib/run_test.go @@ -10,3 +10,10 @@ func Test_buildRunPayload(t *testing.T) { t.Fatalf("did not get expected result, got %q", got) } } + +func Test_buildRunTriggerPayload(t *testing.T) { + got := buildRunTriggerPayload("ws_id") + if got != `{"data":{"relationships":{"sourceable":{"data":{"id":"ws_id","type":"workspaces"}}}}}` { + t.Fatalf("did not get expected result, got %q", got) + } +} From 464d0f9b0ebda57e2e1302f04db9817f1b51c66c Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 16:10:16 +0800 Subject: [PATCH 2/6] better error messages and simpler payload construction --- lib/run.go | 20 ++++++++++---------- lib/run_test.go | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/run.go b/lib/run.go index 9cbd1a8..cd75bcf 100644 --- a/lib/run.go +++ b/lib/run.go @@ -25,15 +25,15 @@ func buildRunPayload(message, workspaceID string) string { _, err := data.Object("data") if err != nil { - return "error" + return "unable to create run payload:" + err.Error() } if _, err = data.SetP(message, "data.attributes.message"); err != nil { - return "unable to process attribute for update:" + err.Error() + return "unable to process message for run payload:" + err.Error() } if _, err = data.SetP(workspaceID, "data.relationships.workspace.data.id"); err != nil { - return "unable to process attribute for update:" + err.Error() + return "unable to process workspace ID for run payload:" + err.Error() } return data.String() @@ -56,15 +56,15 @@ func buildRunTriggerPayload(sourceWorkspaceID string) string { _, err := data.Object("data") if err != nil { - return "error" + return "unable to create run trigger payload:" + err.Error() } - if _, err = data.SetP(sourceWorkspaceID, "data.relationships.sourceable.data.id"); err != nil { - return "unable to process attribute for update:" + err.Error() - } - - if _, err = data.SetP("workspaces", "data.relationships.sourceable.data.type"); err != nil { - return "unable to process attribute for update:" + err.Error() + workspaceObject := gabs.Wrap(map[string]any{ + "type": "workspaces", + "id": sourceWorkspaceID, + }) + if _, err = data.SetP(workspaceObject, "data.relationships.sourceable.data"); err != nil { + return "unable to complete run trigger payload:" + err.Error() } return data.String() diff --git a/lib/run_test.go b/lib/run_test.go index 4ce7a3a..8ed8362 100644 --- a/lib/run_test.go +++ b/lib/run_test.go @@ -7,13 +7,13 @@ import ( func Test_buildRunPayload(t *testing.T) { got := buildRunPayload("my message", "ws_id") if got != `{"data":{"attributes":{"message":"my message"},"relationships":{"workspace":{"data":{"id":"ws_id"}}}}}` { - t.Fatalf("did not get expected result, got %q", got) + t.Fatalf("did not get expected result, got %s", got) } } func Test_buildRunTriggerPayload(t *testing.T) { got := buildRunTriggerPayload("ws_id") if got != `{"data":{"relationships":{"sourceable":{"data":{"id":"ws_id","type":"workspaces"}}}}}` { - t.Fatalf("did not get expected result, got %q", got) + t.Fatalf("did not get expected result, got %s", got) } } From 2a011ac7345c030231924dcf532845c44641f014 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 18:05:47 +0800 Subject: [PATCH 3/6] add ListRunTriggers and FindRunTrigger functions --- go.mod | 3 ++ lib/run.go | 31 ------------ lib/run_test.go | 7 --- lib/runtrigger.go | 110 +++++++++++++++++++++++++++++++++++++++++ lib/runtrigger_test.go | 55 +++++++++++++++++++++ 5 files changed, 168 insertions(+), 38 deletions(-) create mode 100644 lib/runtrigger.go create mode 100644 lib/runtrigger_test.go diff --git a/go.mod b/go.mod index 06fd216..10b4362 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,19 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.1 ) require ( github.com/chzyer/readline v1.5.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/lib/run.go b/lib/run.go index cd75bcf..bdf62d1 100644 --- a/lib/run.go +++ b/lib/run.go @@ -38,34 +38,3 @@ func buildRunPayload(message, workspaceID string) string { return data.String() } - -type RunTriggerConfig struct { - WorkspaceID string - SourceWorkspaceID string -} - -func CreateRunTrigger(config RunTriggerConfig) error { - u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") - payload := buildRunTriggerPayload(config.SourceWorkspaceID) - _ = callAPI(http.MethodPost, u.String(), payload, nil) - return nil -} - -func buildRunTriggerPayload(sourceWorkspaceID string) string { - data := gabs.New() - - _, err := data.Object("data") - if err != nil { - return "unable to create run trigger payload:" + err.Error() - } - - workspaceObject := gabs.Wrap(map[string]any{ - "type": "workspaces", - "id": sourceWorkspaceID, - }) - if _, err = data.SetP(workspaceObject, "data.relationships.sourceable.data"); err != nil { - return "unable to complete run trigger payload:" + err.Error() - } - - return data.String() -} diff --git a/lib/run_test.go b/lib/run_test.go index 8ed8362..c856cdf 100644 --- a/lib/run_test.go +++ b/lib/run_test.go @@ -10,10 +10,3 @@ func Test_buildRunPayload(t *testing.T) { t.Fatalf("did not get expected result, got %s", got) } } - -func Test_buildRunTriggerPayload(t *testing.T) { - got := buildRunTriggerPayload("ws_id") - if got != `{"data":{"relationships":{"sourceable":{"data":{"id":"ws_id","type":"workspaces"}}}}}` { - t.Fatalf("did not get expected result, got %s", got) - } -} diff --git a/lib/runtrigger.go b/lib/runtrigger.go new file mode 100644 index 0000000..a83dbcd --- /dev/null +++ b/lib/runtrigger.go @@ -0,0 +1,110 @@ +package lib + +import ( + "fmt" + "io" + "net/http" + "time" + + "github.com/Jeffail/gabs/v2" +) + +type RunTriggerConfig struct { + WorkspaceID string + SourceWorkspaceID string +} + +func CreateRunTrigger(config RunTriggerConfig) error { + u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") + payload := buildRunTriggerPayload(config.SourceWorkspaceID) + _ = callAPI(http.MethodPost, u.String(), payload, nil) + return nil +} + +func buildRunTriggerPayload(sourceWorkspaceID string) string { + data := gabs.New() + _, err := data.Object("data") + if err != nil { + return "unable to create run trigger payload:" + err.Error() + } + + workspaceObject := gabs.Wrap(map[string]any{ + "type": "workspaces", + "id": sourceWorkspaceID, + }) + if _, err = data.SetP(workspaceObject, "data.relationships.sourceable.data"); err != nil { + return "unable to complete run trigger payload:" + err.Error() + } + + return data.String() +} + +type FindRunTriggerConfig struct { + SourceID string + WorkspaceID string +} + +// FindRunTrigger searches all the run triggers inbound to the given WorkspaceID. If a run trigger is configured for +// the given SourceID, that trigger is returned. Otherwise, nil is returned. +func FindRunTrigger(config FindRunTriggerConfig) (*RunTrigger, error) { + triggers, err := ListRunTriggers(ListRunTriggerConfig{ + WorkspaceID: config.WorkspaceID, + Type: "inbound", + }) + if err != nil { + return nil, fmt.Errorf("failed to list run triggers: %w", err) + } + for _, t := range triggers { + if t.SourceID == config.SourceID { + return &t, nil + } + } + return nil, nil +} + +type RunTrigger struct { + CreatedAt time.Time + SourceName string + SourceID string + WorkspaceName string + WorkspaceID string +} + +type ListRunTriggerConfig struct { + WorkspaceID string + Type string // must be either "inbound" or "outbound" +} + +// ListRunTriggers returns a list of run triggers configured for the given workspace +// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#list-run-triggers +func ListRunTriggers(config ListRunTriggerConfig) ([]RunTrigger, error) { + u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers?filter%5Brun-trigger%5D%5Btype%5D=" + config.Type) + resp := callAPI(http.MethodGet, u.String(), "", nil) + triggers, err := parseRunTriggerListResponse(resp.Body) + if err != nil { + return nil, err + } + return triggers, nil +} + +func parseRunTriggerListResponse(r io.Reader) ([]RunTrigger, error) { + parsed, err := gabs.ParseJSONBuffer(r) + if err != nil { + return nil, fmt.Errorf("failed to parse response data: %w", err) + } + + attributes := parsed.Search("data", "*").Children() + triggers := make([]RunTrigger, len(attributes)) + for i, attr := range attributes { + trigger := RunTrigger{ + SourceID: attr.Path("relationships.sourceable.data.id").Data().(string), + SourceName: attr.Path("attributes.sourceable-name").Data().(string), + WorkspaceID: attr.Path("relationships.workspace.data.id").Data().(string), + WorkspaceName: attr.Path("attributes.workspace-name").Data().(string), + } + createdAt, _ := time.Parse(time.RFC3339, attr.Path("attributes.created-at").Data().(string)) + trigger.CreatedAt = createdAt + triggers[i] = trigger + } + return triggers, nil +} diff --git a/lib/runtrigger_test.go b/lib/runtrigger_test.go new file mode 100644 index 0000000..59bb6a4 --- /dev/null +++ b/lib/runtrigger_test.go @@ -0,0 +1,55 @@ +package lib + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func Test_buildRunTriggerPayload(t *testing.T) { + got := buildRunTriggerPayload("ws_id") + if got != `{"data":{"relationships":{"sourceable":{"data":{"id":"ws_id","type":"workspaces"}}}}}` { + t.Fatalf("did not get expected result, got %s", got) + } +} + +func Test_parseRunTriggerListResponse(t *testing.T) { + r := bytes.NewReader([]byte(listTriggerSampleBody)) + triggers, err := parseRunTriggerListResponse(r) + require.NoError(t, err) + require.Equal(t, triggers[0].WorkspaceID, "ws-abcdefghijklmnop") + require.Equal(t, triggers[0].SourceID, "ws-qrstuvwxyzABCDEF") + require.Equal(t, triggers[0].WorkspaceName, "a-workspace-name") + require.Equal(t, triggers[0].SourceName, "source-ws-1") + require.Equal(t, triggers[0].CreatedAt, time.Date(2023, 6, 20, 8, 56, 50, 996e6, time.UTC)) +} + +const listTriggerSampleBody = `{ + "data": [ + { + "id": "rt-abcdefghijklmnop", + "type": "run-triggers", + "attributes": { + "workspace-name": "a-workspace-name", + "sourceable-name": "source-ws-1", + "created-at": "2023-06-20T08:56:50.996Z" + }, + "relationships": { + "workspace": { + "data": { + "id": "ws-abcdefghijklmnop", + "type": "workspaces" + } + }, + "sourceable": { + "data": { + "id": "ws-qrstuvwxyzABCDEF", + "type": "workspaces" + } + } + } + } + ] +}` From 70a5382fff180fd684be0041521c0ba1c7c8f811 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 18:09:37 +0800 Subject: [PATCH 4/6] rename for consistency --- lib/runtrigger.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/runtrigger.go b/lib/runtrigger.go index a83dbcd..98794a8 100644 --- a/lib/runtrigger.go +++ b/lib/runtrigger.go @@ -40,12 +40,12 @@ func buildRunTriggerPayload(sourceWorkspaceID string) string { } type FindRunTriggerConfig struct { - SourceID string - WorkspaceID string + SourceWorkspaceID string + WorkspaceID string } // FindRunTrigger searches all the run triggers inbound to the given WorkspaceID. If a run trigger is configured for -// the given SourceID, that trigger is returned. Otherwise, nil is returned. +// the given SourceWorkspaceID, that trigger is returned. Otherwise, nil is returned. func FindRunTrigger(config FindRunTriggerConfig) (*RunTrigger, error) { triggers, err := ListRunTriggers(ListRunTriggerConfig{ WorkspaceID: config.WorkspaceID, @@ -55,7 +55,7 @@ func FindRunTrigger(config FindRunTriggerConfig) (*RunTrigger, error) { return nil, fmt.Errorf("failed to list run triggers: %w", err) } for _, t := range triggers { - if t.SourceID == config.SourceID { + if t.SourceID == config.SourceWorkspaceID { return &t, nil } } From 9f414ddf415dfcceab92287c966d14593fe04dcf Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 18:26:21 +0800 Subject: [PATCH 5/6] use the url.URL package's SetParam --- lib/runtrigger.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/runtrigger.go b/lib/runtrigger.go index 98794a8..d0361ab 100644 --- a/lib/runtrigger.go +++ b/lib/runtrigger.go @@ -78,7 +78,9 @@ type ListRunTriggerConfig struct { // ListRunTriggers returns a list of run triggers configured for the given workspace // https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#list-run-triggers func ListRunTriggers(config ListRunTriggerConfig) ([]RunTrigger, error) { - u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers?filter%5Brun-trigger%5D%5Btype%5D=" + config.Type) + u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") + u.SetParam(paramFilterRunTriggerType, config.Type) + resp := callAPI(http.MethodGet, u.String(), "", nil) triggers, err := parseRunTriggerListResponse(resp.Body) if err != nil { From c4f5f6a7d0e26db02400e0c0498364d57895bb54 Mon Sep 17 00:00:00 2001 From: briskt <3172830+briskt@users.noreply.github.com> Date: Tue, 20 Jun 2023 19:33:35 +0800 Subject: [PATCH 6/6] add paramFilterRunTriggerType and TestNewTfcUrl --- lib/url.go | 2 ++ lib/url_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 lib/url_test.go diff --git a/lib/url.go b/lib/url.go index 0ff9e73..0d227df 100644 --- a/lib/url.go +++ b/lib/url.go @@ -14,6 +14,7 @@ const ( paramFilterWorkspaceName = "filter[workspace][name]" paramPageSize = "page[size]" paramPageNumber = "page[number]" + paramFilterRunTriggerType = "filter[run-trigger][type]" paramSearchName = "search[name]" ) @@ -21,6 +22,7 @@ type TfcUrl struct { url.URL } +// NewTfcUrl creates a url.URL object for the Terraform Cloud API. func NewTfcUrl(path string) TfcUrl { newURL, _ := url.Parse(baseURL + path) v := url.Values{} diff --git a/lib/url_test.go b/lib/url_test.go new file mode 100644 index 0000000..00b5b89 --- /dev/null +++ b/lib/url_test.go @@ -0,0 +1,15 @@ +package lib + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewTfcUrl(t *testing.T) { + orgs := NewTfcUrl("/organizations") + require.Equal(t, baseURL+"/organizations", orgs.String()) + + withQuery := NewTfcUrl("/organizations?q=foo") + require.Equal(t, baseURL+"/organizations", withQuery.String()) +}