From 4961bcb178df3a75328ca3802060e6136dcae8b8 Mon Sep 17 00:00:00 2001 From: Marcin Wyszynski Date: Thu, 16 Nov 2023 21:39:07 +0100 Subject: [PATCH] Introduce support for context hooks (#443) --- .vscode/settings.json | 10 +- docs/data-sources/context.md | 14 ++ docs/resources/context.md | 11 ++ spacelift/data_context.go | 88 ++++++++++ spacelift/data_context_test.go | 33 ++++ spacelift/internal/structs/context.go | 13 ++ spacelift/internal/structs/context_input.go | 27 +++ spacelift/resource_context.go | 179 +++++++++++++++++--- spacelift/resource_context_test.go | 33 ++++ 9 files changed, 387 insertions(+), 21 deletions(-) create mode 100644 spacelift/internal/structs/context_input.go diff --git a/.vscode/settings.json b/.vscode/settings.json index d1152d05..f5e8279f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "go.testEnvFile": "${workspaceFolder}/test.env" -} \ No newline at end of file + "go.testEnvFile": "${workspaceFolder}/test.env", + "cSpell.words": [ + "autoretry", + "datacenter", + "pulumi", + "Spacelift" + ] +} diff --git a/docs/data-sources/context.md b/docs/data-sources/context.md index 6041edbd..12b1ccc8 100644 --- a/docs/data-sources/context.md +++ b/docs/data-sources/context.md @@ -25,6 +25,20 @@ data "spacelift_context" "prod-k8s-ie" { - `context_id` (String) immutable ID (slug) of the context +### Optional + +- `after_apply` (List of String) List of after-apply scripts +- `after_destroy` (List of String) List of after-destroy scripts +- `after_init` (List of String) List of after-init scripts +- `after_perform` (List of String) List of after-perform scripts +- `after_plan` (List of String) List of after-plan scripts +- `after_run` (List of String) List of after-run scripts +- `before_apply` (List of String) List of before-apply scripts +- `before_destroy` (List of String) List of before-destroy scripts +- `before_init` (List of String) List of before-init scripts +- `before_perform` (List of String) List of before-perform scripts +- `before_plan` (List of String) List of before-plan scripts + ### Read-Only - `description` (String) free-form context description for users diff --git a/docs/resources/context.md b/docs/resources/context.md index 7b09268c..4792469b 100644 --- a/docs/resources/context.md +++ b/docs/resources/context.md @@ -28,6 +28,17 @@ resource "spacelift_context" "prod-k8s-ie" { ### Optional +- `after_apply` (List of String) List of after-apply scripts +- `after_destroy` (List of String) List of after-destroy scripts +- `after_init` (List of String) List of after-init scripts +- `after_perform` (List of String) List of after-perform scripts +- `after_plan` (List of String) List of after-plan scripts +- `after_run` (List of String) List of after-run scripts +- `before_apply` (List of String) List of before-apply scripts +- `before_destroy` (List of String) List of before-destroy scripts +- `before_init` (List of String) List of before-init scripts +- `before_perform` (List of String) List of before-perform scripts +- `before_plan` (List of String) List of before-plan scripts - `description` (String) Free-form context description for users - `labels` (Set of String) - `space_id` (String) ID (slug) of the space the context is in diff --git a/spacelift/data_context.go b/spacelift/data_context.go index 0393671d..e2c1be48 100644 --- a/spacelift/data_context.go +++ b/spacelift/data_context.go @@ -23,6 +23,82 @@ func dataContext() *schema.Resource { ReadContext: dataContextRead, Schema: map[string]*schema.Schema{ + "after_apply": { + Type: schema.TypeList, + Description: "List of after-apply scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "after_destroy": { + Type: schema.TypeList, + Description: "List of after-destroy scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "after_init": { + Type: schema.TypeList, + Description: "List of after-init scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "after_perform": { + Type: schema.TypeList, + Description: "List of after-perform scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "after_plan": { + Type: schema.TypeList, + Description: "List of after-plan scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "after_run": { + Type: schema.TypeList, + Description: "List of after-run scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "before_apply": { + Type: schema.TypeList, + Description: "List of before-apply scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "before_destroy": { + Type: schema.TypeList, + Description: "List of before-destroy scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "before_init": { + Type: schema.TypeList, + Description: "List of before-init scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "before_perform": { + Type: schema.TypeList, + Description: "List of before-perform scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, + "before_plan": { + Type: schema.TypeList, + Description: "List of before-plan scripts", + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + }, "context_id": { Type: schema.TypeString, Description: "immutable ID (slug) of the context", @@ -84,5 +160,17 @@ func dataContextRead(ctx context.Context, d *schema.ResourceData, meta interface } d.Set("labels", labels) + d.Set("after_apply", context.Hooks.AfterApply) + d.Set("after_destroy", context.Hooks.AfterDestroy) + d.Set("after_init", context.Hooks.AfterInit) + d.Set("after_perform", context.Hooks.AfterPerform) + d.Set("after_plan", context.Hooks.AfterPlan) + d.Set("after_run", context.Hooks.AfterRun) + d.Set("before_apply", context.Hooks.BeforeApply) + d.Set("before_destroy", context.Hooks.BeforeDestroy) + d.Set("before_init", context.Hooks.BeforeInit) + d.Set("before_perform", context.Hooks.BeforePerform) + d.Set("before_plan", context.Hooks.BeforePlan) + return nil } diff --git a/spacelift/data_context_test.go b/spacelift/data_context_test.go index 0bc9bfec..09af3b2c 100644 --- a/spacelift/data_context_test.go +++ b/spacelift/data_context_test.go @@ -20,6 +20,17 @@ func TestContextData(t *testing.T) { name = "Provider test context %s" description = "description" labels = ["one", "two"] + after_apply = ["after_apply"] + after_destroy = ["after_destroy"] + after_init = ["after_init"] + after_perform = ["after_perform"] + after_plan = ["after_plan"] + after_run = ["after_run"] + before_apply = ["before_apply"] + before_destroy = ["before_destroy"] + before_init = ["before_init"] + before_perform = ["before_perform"] + before_plan = ["before_plan"] } data "spacelift_context" "test" { context_id = spacelift_context.test.id @@ -31,6 +42,28 @@ func TestContextData(t *testing.T) { Attribute("name", StartsWith("Provider test context")), Attribute("description", Equals("description")), SetEquals("labels", "one", "two"), + Attribute("after_apply.#", Equals("1")), + Attribute("after_apply.0", Equals("after_apply")), + Attribute("after_destroy.#", Equals("1")), + Attribute("after_destroy.0", Equals("after_destroy")), + Attribute("after_init.#", Equals("1")), + Attribute("after_init.0", Equals("after_init")), + Attribute("after_perform.#", Equals("1")), + Attribute("after_perform.0", Equals("after_perform")), + Attribute("after_plan.#", Equals("1")), + Attribute("after_plan.0", Equals("after_plan")), + Attribute("after_run.#", Equals("1")), + Attribute("after_run.0", Equals("after_run")), + Attribute("before_apply.#", Equals("1")), + Attribute("before_apply.0", Equals("before_apply")), + Attribute("before_destroy.#", Equals("1")), + Attribute("before_destroy.0", Equals("before_destroy")), + Attribute("before_init.#", Equals("1")), + Attribute("before_init.0", Equals("before_init")), + Attribute("before_perform.#", Equals("1")), + Attribute("before_perform.0", Equals("before_perform")), + Attribute("before_plan.#", Equals("1")), + Attribute("before_plan.0", Equals("before_plan")), ), }}) }) diff --git a/spacelift/internal/structs/context.go b/spacelift/internal/structs/context.go index e183491d..e8dd0177 100644 --- a/spacelift/internal/structs/context.go +++ b/spacelift/internal/structs/context.go @@ -7,4 +7,17 @@ type Context struct { Labels []string `graphql:"labels"` Name string `graphql:"name"` Space string `graphql:"space"` + Hooks struct { + AfterApply []string `graphql:"afterApply"` + AfterDestroy []string `graphql:"afterDestroy"` + AfterInit []string `graphql:"afterInit"` + AfterPerform []string `graphql:"afterPerform"` + AfterPlan []string `graphql:"afterPlan"` + AfterRun []string `graphql:"afterRun"` + BeforeApply []string `graphql:"beforeApply"` + BeforeDestroy []string `graphql:"beforeDestroy"` + BeforeInit []string `graphql:"beforeInit"` + BeforePerform []string `graphql:"beforePerform"` + BeforePlan []string `graphql:"beforePlan"` + } `graphql:"hooks"` } diff --git a/spacelift/internal/structs/context_input.go b/spacelift/internal/structs/context_input.go new file mode 100644 index 00000000..0b6ffa11 --- /dev/null +++ b/spacelift/internal/structs/context_input.go @@ -0,0 +1,27 @@ +package structs + +import "github.com/shurcooL/graphql" + +// ContextInput represents the input required to create or update a Context. +type ContextInput struct { + Name graphql.String `json:"name"` + Description *graphql.String `json:"description"` + Hooks *HooksInput `json:"hooks"` + Labels *[]graphql.String `json:"labels"` + Space *graphql.ID `json:"space"` +} + +// HooksInput represents the input required to create or update Hooks. +type HooksInput struct { + AfterApply []graphql.String `json:"afterApply"` + AfterDestroy []graphql.String `json:"afterDestroy"` + AfterInit []graphql.String `json:"afterInit"` + AfterPerform []graphql.String `json:"afterPerform"` + AfterPlan []graphql.String `json:"afterPlan"` + AfterRun []graphql.String `json:"afterRun"` + BeforeApply []graphql.String `json:"beforeApply"` + BeforeDestroy []graphql.String `json:"beforeDestroy"` + BeforeInit []graphql.String `json:"beforeInit"` + BeforePerform []graphql.String `json:"beforePerform"` + BeforePlan []graphql.String `json:"beforePlan"` +} diff --git a/spacelift/resource_context.go b/spacelift/resource_context.go index d15abd21..caafae0e 100644 --- a/spacelift/resource_context.go +++ b/spacelift/resource_context.go @@ -31,6 +31,105 @@ func resourceContext() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "after_apply": { + Type: schema.TypeList, + Description: "List of after-apply scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "after_destroy": { + Type: schema.TypeList, + Description: "List of after-destroy scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "after_init": { + Type: schema.TypeList, + Description: "List of after-init scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "after_perform": { + Type: schema.TypeList, + Description: "List of after-perform scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "after_plan": { + Type: schema.TypeList, + Description: "List of after-plan scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "after_run": { + Type: schema.TypeList, + Description: "List of after-run scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "before_apply": { + Type: schema.TypeList, + Description: "List of before-apply scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "before_destroy": { + Type: schema.TypeList, + Description: "List of before-destroy scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "before_init": { + Type: schema.TypeList, + Description: "List of before-init scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "before_perform": { + Type: schema.TypeList, + Description: "List of before-perform scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + "before_plan": { + Type: schema.TypeList, + Description: "List of before-plan scripts", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, "description": { Type: schema.TypeString, Description: "Free-form context description for users", @@ -63,22 +162,20 @@ func resourceContext() *schema.Resource { func resourceContextCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var mutation struct { - CreateContext structs.Context `graphql:"contextCreate(name: $name, description: $description, labels: $labels, space: $space)"` + CreateContext structs.Context `graphql:"contextCreateV2(input: $input)"` } - variables := map[string]interface{}{ - "name": toString(d.Get("name")), - "description": (*graphql.String)(nil), - "labels": (*[]graphql.String)(nil), - "space": (*graphql.ID)(nil), + input := structs.ContextInput{ + Name: toString(d.Get("name")), + Hooks: buildHooksInput(d), } if description, ok := d.GetOk("description"); ok { - variables["description"] = toOptionalString(description) + input.Description = toOptionalString(description) } if spaceID, ok := d.GetOk("space_id"); ok { - variables["space"] = graphql.NewID(spaceID) + input.Space = graphql.NewID(spaceID) } if labelSet, ok := d.Get("labels").(*schema.Set); ok { @@ -88,9 +185,11 @@ func resourceContextCreate(ctx context.Context, d *schema.ResourceData, meta int labels = append(labels, graphql.String(label.(string))) } - variables["labels"] = &labels + input.Labels = &labels } + variables := map[string]interface{}{"input": input} + if err := meta.(*internal.Client).Mutate(ctx, "ContextCreate", &mutation, variables); err != nil { return diag.Errorf("could not create context: %v", internal.FromSpaceliftError(err)) } @@ -129,28 +228,37 @@ func resourceContextRead(ctx context.Context, d *schema.ResourceData, meta inter d.Set("labels", labels) d.Set("space_id", context.Space) + d.Set("after_apply", context.Hooks.AfterApply) + d.Set("after_destroy", context.Hooks.AfterDestroy) + d.Set("after_init", context.Hooks.AfterInit) + d.Set("after_perform", context.Hooks.AfterPerform) + d.Set("after_plan", context.Hooks.AfterPlan) + d.Set("after_run", context.Hooks.AfterRun) + d.Set("before_apply", context.Hooks.BeforeApply) + d.Set("before_destroy", context.Hooks.BeforeDestroy) + d.Set("before_init", context.Hooks.BeforeInit) + d.Set("before_perform", context.Hooks.BeforePerform) + d.Set("before_plan", context.Hooks.BeforePlan) + return nil } func resourceContextUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var mutation struct { - UpdateContext structs.Context `graphql:"contextUpdate(id: $id, name: $name, description: $description, labels: $labels, space: $space)"` + UpdateContext structs.Context `graphql:"contextUpdateV2(id: $id, input: $input)"` } - variables := map[string]interface{}{ - "id": toID(d.Id()), - "name": toString(d.Get("name")), - "description": (*graphql.String)(nil), - "labels": (*[]graphql.String)(nil), - "space": (*graphql.ID)(nil), + input := structs.ContextInput{ + Name: toString(d.Get("name")), + Hooks: buildHooksInput(d), } if description, ok := d.GetOk("description"); ok { - variables["description"] = toOptionalString(description) + input.Description = toOptionalString(description) } if spaceID, ok := d.GetOk("space_id"); ok { - variables["space"] = graphql.NewID(spaceID) + input.Space = graphql.NewID(spaceID) } if labelSet, ok := d.Get("labels").(*schema.Set); ok { @@ -160,11 +268,16 @@ func resourceContextUpdate(ctx context.Context, d *schema.ResourceData, meta int labels = append(labels, graphql.String(label.(string))) } - variables["labels"] = &labels + input.Labels = &labels } var ret diag.Diagnostics + variables := map[string]interface{}{ + "id": toID(d.Id()), + "input": input, + } + if err := meta.(*internal.Client).Mutate(ctx, "ContextUpdate", &mutation, variables); err != nil { ret = append(ret, diag.Errorf("could not update context: %v", internal.FromSpaceliftError(err))...) } @@ -189,3 +302,31 @@ func resourceContextDelete(ctx context.Context, d *schema.ResourceData, meta int return nil } + +func buildHooksInput(d *schema.ResourceData) *structs.HooksInput { + return &structs.HooksInput{ + AfterApply: gqlStringList(d, "after_apply"), + AfterDestroy: gqlStringList(d, "after_destroy"), + AfterInit: gqlStringList(d, "after_init"), + AfterPerform: gqlStringList(d, "after_perform"), + AfterPlan: gqlStringList(d, "after_plan"), + AfterRun: gqlStringList(d, "after_run"), + BeforeApply: gqlStringList(d, "before_apply"), + BeforeDestroy: gqlStringList(d, "before_destroy"), + BeforeInit: gqlStringList(d, "before_init"), + BeforePerform: gqlStringList(d, "before_perform"), + BeforePlan: gqlStringList(d, "before_plan"), + } +} + +func gqlStringList(d *schema.ResourceData, key string) []graphql.String { + ret := []graphql.String{} + + if list, ok := d.GetOk(key); ok { + for _, item := range list.([]interface{}) { + ret = append(ret, graphql.String(item.(string))) + } + } + + return ret +} diff --git a/spacelift/resource_context_test.go b/spacelift/resource_context_test.go index 7ef831a6..1abf3e45 100644 --- a/spacelift/resource_context_test.go +++ b/spacelift/resource_context_test.go @@ -22,6 +22,17 @@ func TestContextResource(t *testing.T) { name = "Provider test context %s" description = "%s" labels = ["one", "two"] + after_apply = ["after_apply"] + after_destroy = ["after_destroy"] + after_init = ["after_init"] + after_perform = ["after_perform"] + after_plan = ["after_plan"] + after_run = ["after_run"] + before_apply = ["before_apply"] + before_destroy = ["before_destroy"] + before_init = ["before_init"] + before_perform = ["before_perform"] + before_plan = ["before_plan"] } `, randomID, description) } @@ -35,6 +46,28 @@ func TestContextResource(t *testing.T) { Attribute("name", StartsWith("Provider test context")), Attribute("description", Equals("old description")), SetEquals("labels", "one", "two"), + Attribute("after_apply.#", Equals("1")), + Attribute("after_apply.0", Equals("after_apply")), + Attribute("after_destroy.#", Equals("1")), + Attribute("after_destroy.0", Equals("after_destroy")), + Attribute("after_init.#", Equals("1")), + Attribute("after_init.0", Equals("after_init")), + Attribute("after_perform.#", Equals("1")), + Attribute("after_perform.0", Equals("after_perform")), + Attribute("after_plan.#", Equals("1")), + Attribute("after_plan.0", Equals("after_plan")), + Attribute("after_run.#", Equals("1")), + Attribute("after_run.0", Equals("after_run")), + Attribute("before_apply.#", Equals("1")), + Attribute("before_apply.0", Equals("before_apply")), + Attribute("before_destroy.#", Equals("1")), + Attribute("before_destroy.0", Equals("before_destroy")), + Attribute("before_init.#", Equals("1")), + Attribute("before_init.0", Equals("before_init")), + Attribute("before_perform.#", Equals("1")), + Attribute("before_perform.0", Equals("before_perform")), + Attribute("before_plan.#", Equals("1")), + Attribute("before_plan.0", Equals("before_plan")), ), }, {