From 1cfb83113492d6b3459eb48e30c0b7699fd3aa05 Mon Sep 17 00:00:00 2001 From: Piotr Truszkowski Date: Tue, 28 May 2024 15:54:31 +0200 Subject: [PATCH] feat: new resource `spacelift_gitlab_integration` (#557) * refactor: moved CustomVCSInput and CustomVCSUpdateInput to separate package vcs * refactor: moved GitLab schema field name constants to gitlab_integration.go * feat: implemented spacelift_gitlab_integration resource * test: added tests for spacelift_gitlab_integration resource * docs: added examples and docs for spacelift_gitlab_integration resource * chore: added .envrc for easier setup of environment variables from .env file * cicd: added SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_TOKEN and SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_TOKEN to GitHub test workflows * fix: fix typo in GraphQL query for GitLabIntegration and add ForceNew flag to isDefault field in schema in spacelift/resource_gitlab_integration.go * refactor: rename "webhookURL" field to "webhookUrl" in GitLabIntegration struct and remove "Token" field * refactor: changes to spacelift/resource_gitlab_integration.go * add CustomizeDiff function ensure space is set to root if the integration should be the default * make gitLabUserFacingHost required and removed code to derive it from gitLabAPIHost if not set * set default value for gitLabSpaceID to "root" * docs: updated docs for spacelift_gitlab_integration * tests: updated tests for spacelift_gitlab_integration * added .envrc into .gitignore * minor fixes: naming, messaging, verifying --------- Co-authored-by: Thomas Meckel --- .github/workflows/test-prod.yml | 4 +- .github/workflows/test.yml | 4 +- .gitignore | 1 + docs/resources/gitlab_integration.md | 62 +++++ .../spacelift_gitlab_integration/import.sh | 1 + .../spacelift_gitlab_integration/resource.tf | 15 ++ spacelift/config_test.go | 2 + spacelift/data_gitlab_integration.go | 59 ++--- spacelift/gitlab_integration.go | 16 ++ .../bitbucket_datacenter_integration.go | 19 -- .../internal/structs/gitlab_integration.go | 18 ++ spacelift/internal/structs/vcs/input.go | 12 + spacelift/internal/structs/vcs/update.go | 11 + spacelift/provider.go | 1 + ...source_bitbucket_datacenter_integration.go | 5 +- spacelift/resource_gitlab_integration.go | 223 ++++++++++++++++++ spacelift/resource_gitlab_integration_test.go | 195 +++++++++++++++ 17 files changed, 589 insertions(+), 59 deletions(-) create mode 100644 docs/resources/gitlab_integration.md create mode 100644 examples/resources/spacelift_gitlab_integration/import.sh create mode 100644 examples/resources/spacelift_gitlab_integration/resource.tf create mode 100644 spacelift/gitlab_integration.go create mode 100644 spacelift/internal/structs/gitlab_integration.go create mode 100644 spacelift/internal/structs/vcs/input.go create mode 100644 spacelift/internal/structs/vcs/update.go create mode 100644 spacelift/resource_gitlab_integration.go create mode 100644 spacelift/resource_gitlab_integration_test.go diff --git a/.github/workflows/test-prod.yml b/.github/workflows/test-prod.yml index 3a1e2b6d..da045a33 100644 --- a/.github/workflows/test-prod.yml +++ b/.github/workflows/test-prod.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Install Go uses: actions/setup-go@v5 with: { go-version-file: go.mod } @@ -83,12 +83,14 @@ jobs: SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_NAME: "GitLab Default" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_ID: "gitlab-default" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_APIHOST: "https://gitlab.com" + SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_TOKEN: ${{ secrets.COMMON_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_TOKEN }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKSECRET: ${{ secrets.PROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKSECRET }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKURL: ${{ secrets.PROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKURL }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_NAME: "GitLab Space Level" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_ID: "gitlab-space-level" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_SPACE: "tests-01HPE6H08F8HR8PJR78DPYR3TC" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_APIHOST: "https://gitlab.com" + SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_TOKEN: ${{ secrets.COMMON_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_TOKEN }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKSECRET: ${{ secrets.PROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKSECRET }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKURL: ${{ secrets.PROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKURL }} SPACELIFT_PROVIDER_TEST_SOURCECODE_AZUREDEVOPS_REPOSITORY_NAME: "spacelift-ci" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b90503b..aaf05b7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - + - name: Install Go uses: actions/setup-go@v5 with: { go-version-file: go.mod } @@ -85,12 +85,14 @@ jobs: SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_NAME: "GitLab Default" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_ID: "gitlab-default" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_APIHOST: "https://gitlab.com" + SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_TOKEN: ${{ secrets.COMMON_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_TOKEN }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKSECRET: ${{ secrets.PREPROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKSECRET }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKURL: ${{ secrets.PREPROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_DEFAULT_WEBHOOKURL }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_NAME: "GitLab Space Level" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_ID: "gitlab-space-level" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_SPACE: "tests-01HPE6ENR1AZZ638QSRQRVW4DH" SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_APIHOST: "https://gitlab.com" + SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_TOKEN: ${{ secrets.COMMON_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_TOKEN }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKSECRET: ${{ secrets.PREPROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKSECRET }} SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKURL: ${{ secrets.PREPROD_SPACELIFT_PROVIDER_TEST_SOURCECODE_GITLAB_SPACELEVEL_WEBHOOKURL }} SPACELIFT_PROVIDER_TEST_SOURCECODE_AZUREDEVOPS_REPOSITORY_NAME: "spacelift-ci" diff --git a/.gitignore b/.gitignore index 114c876f..c614ff05 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ key.* dist/ test.env +.envrc diff --git a/docs/resources/gitlab_integration.md b/docs/resources/gitlab_integration.md new file mode 100644 index 00000000..d6bd6063 --- /dev/null +++ b/docs/resources/gitlab_integration.md @@ -0,0 +1,62 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_gitlab_integration Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_gitlab_integration represents an integration with a GitLab instance +--- + +# spacelift_gitlab_integration (Resource) + +`spacelift_gitlab_integration` represents an integration with a GitLab instance + +## Example Usage + +```terraform +resource "spacelift_gitlab_integration" "example" { + name = "GitLab integration (public)" + space_id = "root" + api_host = "https://mygitlab.myorg.com" + user_facing_host = "https://mygitlab.myorg.com" + private_token = "gitlab-token" +} + +resource "spacelift_gitlab_integration" "private-example" { + name = "GitLab integration (private)" + is_default = true + api_host = "private://mygitlab" + user_facing_host = "https://mygitlab.myorg.com" + private_token = "gitlab-token" +} +``` + + +## Schema + +### Required + +- `api_host` (String) API host URL +- `name` (String) The friendly name of the integration +- `private_token` (String, Sensitive) The GitLab API Token +- `user_facing_host` (String) User facing host URL. + +### Optional + +- `description` (String) Description of the integration +- `is_default` (Boolean) Is the GitLab integration the default for all spaces? If set to `true` the space must be set to `root` in `space_id` or left empty which uses the default +- `labels` (Set of String) Labels to set on the integration +- `space_id` (String) ID (slug) of the space the integration is in; Default: `root` + +### Read-Only + +- `id` (String) GitLab integration id. +- `webhook_secret` (String, Sensitive) Secret for webhooks originating from GitLab repositories +- `webhook_url` (String) URL for webhooks originating from GitLab repositories + +## Import + +Import is supported using the following syntax: + +```shell +terraform import spacelift_gitlab_integration.example spacelift_gitlab_integration_id +``` diff --git a/examples/resources/spacelift_gitlab_integration/import.sh b/examples/resources/spacelift_gitlab_integration/import.sh new file mode 100644 index 00000000..c46c6070 --- /dev/null +++ b/examples/resources/spacelift_gitlab_integration/import.sh @@ -0,0 +1 @@ +terraform import spacelift_gitlab_integration.example spacelift_gitlab_integration_id diff --git a/examples/resources/spacelift_gitlab_integration/resource.tf b/examples/resources/spacelift_gitlab_integration/resource.tf new file mode 100644 index 00000000..709d2791 --- /dev/null +++ b/examples/resources/spacelift_gitlab_integration/resource.tf @@ -0,0 +1,15 @@ +resource "spacelift_gitlab_integration" "example" { + name = "GitLab integration (public)" + space_id = "root" + api_host = "https://mygitlab.myorg.com" + user_facing_host = "https://mygitlab.myorg.com" + private_token = "gitlab-token" +} + +resource "spacelift_gitlab_integration" "private-example" { + name = "GitLab integration (private)" + is_default = true + api_host = "private://mygitlab" + user_facing_host = "https://mygitlab.myorg.com" + private_token = "gitlab-token" +} diff --git a/spacelift/config_test.go b/spacelift/config_test.go index 515f02f7..a1f49e3d 100644 --- a/spacelift/config_test.go +++ b/spacelift/config_test.go @@ -108,6 +108,7 @@ var testConfig struct { Default struct { Name string ID string + Token string APIHost string WebhookSecret string WebhookURL string @@ -117,6 +118,7 @@ var testConfig struct { ID string Space string APIHost string + Token string WebhookSecret string WebhookURL string } diff --git a/spacelift/data_gitlab_integration.go b/spacelift/data_gitlab_integration.go index 689bf52b..0a99dcec 100644 --- a/spacelift/data_gitlab_integration.go +++ b/spacelift/data_gitlab_integration.go @@ -9,19 +9,6 @@ import ( "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" ) -const ( - gitlabId = "id" - gitlabName = "name" - gitlabDescription = "description" - gitlabIsDefault = "is_default" - gitlabLabels = "labels" - gitlabSpaceID = "space_id" - gitlabAppID = "app_id" - gitlabAPIHost = "api_host" - gitlabWebhookSecret = "webhook_secret" - gitlabWebhookURL = "webhook_url" -) - func dataGitlabIntegration() *schema.Resource { return &schema.Resource{ Description: "`spacelift_gitlab_integration` returns details about Gitlab integration", @@ -29,27 +16,27 @@ func dataGitlabIntegration() *schema.Resource { ReadContext: dataGitlabIntegrationRead, Schema: map[string]*schema.Schema{ - gitlabId: { + gitLabID: { Type: schema.TypeString, Description: "Gitlab integration id. If not provided, the default integration will be returned", Optional: true, }, - gitlabName: { + gitLabName: { Type: schema.TypeString, Description: "Gitlab integration name", Computed: true, }, - gitlabDescription: { + gitLabDescription: { Type: schema.TypeString, Description: "Gitlab integration description", Computed: true, }, - gitlabIsDefault: { + gitLabIsDefault: { Type: schema.TypeBool, Description: "Gitlab integration is default", Computed: true, }, - gitlabLabels: { + gitLabLabels: { Type: schema.TypeList, Description: "Gitlab integration labels", Computed: true, @@ -57,22 +44,22 @@ func dataGitlabIntegration() *schema.Resource { Type: schema.TypeString, }, }, - gitlabSpaceID: { + gitLabSpaceID: { Type: schema.TypeString, Description: "Gitlab integration space id", Computed: true, }, - gitlabAPIHost: { + gitLabAPIHost: { Type: schema.TypeString, Description: "Gitlab integration api host", Computed: true, }, - gitlabWebhookSecret: { + gitLabWebhookSecret: { Type: schema.TypeString, Description: "Gitlab integration webhook secret", Computed: true, }, - gitlabWebhookURL: { + gitLabWebhookURL: { Type: schema.TypeString, Description: "Gitlab integration webhook url", Computed: true, @@ -101,7 +88,7 @@ func dataGitlabIntegrationRead(ctx context.Context, d *schema.ResourceData, meta variables := map[string]interface{}{"id": ""} - if id, ok := d.GetOk(gitlabId); ok && id != "" { + if id, ok := d.GetOk(gitLabID); ok && id != "" { variables["id"] = toID(id) } @@ -109,27 +96,27 @@ func dataGitlabIntegrationRead(ctx context.Context, d *schema.ResourceData, meta return diag.Errorf("could not query for gitlab integration: %v", err) } - gitlabIntegration := query.GitlabIntegration - if gitlabIntegration == nil { + gitLabIntegration := query.GitlabIntegration + if gitLabIntegration == nil { return diag.Errorf("gitlab integration not found") } - d.SetId(gitlabIntegration.ID) - d.Set(gitlabAPIHost, gitlabIntegration.APIHost) - d.Set(gitlabWebhookSecret, gitlabIntegration.WebhookSecret) - d.Set(gitlabWebhookURL, gitlabIntegration.WebhookURL) - d.Set(gitlabId, gitlabIntegration.ID) - d.Set(gitlabName, gitlabIntegration.Name) - d.Set(gitlabDescription, gitlabIntegration.Description) - d.Set(gitlabIsDefault, gitlabIntegration.IsDefault) - d.Set(gitlabSpaceID, gitlabIntegration.Space.ID) + d.SetId(gitLabIntegration.ID) + d.Set(gitLabAPIHost, gitLabIntegration.APIHost) + d.Set(gitLabWebhookSecret, gitLabIntegration.WebhookSecret) + d.Set(gitLabWebhookURL, gitLabIntegration.WebhookURL) + d.Set(gitLabID, gitLabIntegration.ID) + d.Set(gitLabName, gitLabIntegration.Name) + d.Set(gitLabDescription, gitLabIntegration.Description) + d.Set(gitLabIsDefault, gitLabIntegration.IsDefault) + d.Set(gitLabSpaceID, gitLabIntegration.Space.ID) labels := schema.NewSet(schema.HashString, []interface{}{}) - for _, label := range gitlabIntegration.Labels { + for _, label := range gitLabIntegration.Labels { labels.Add(label) } - d.Set(gitlabLabels, labels) + d.Set(gitLabLabels, labels) return nil } diff --git a/spacelift/gitlab_integration.go b/spacelift/gitlab_integration.go new file mode 100644 index 00000000..cfcd5637 --- /dev/null +++ b/spacelift/gitlab_integration.go @@ -0,0 +1,16 @@ +package spacelift + +const ( + gitLabID = "id" + gitLabName = "name" + gitLabDescription = "description" + gitLabIsDefault = "is_default" + gitLabLabels = "labels" + gitLabSpaceID = "space_id" + gitLabUserFacingHost = "user_facing_host" + gitLabAppID = "app_id" + gitLabAPIHost = "api_host" + gitLabToken = "private_token" + gitLabWebhookURL = "webhook_url" + gitLabWebhookSecret = "webhook_secret" +) diff --git a/spacelift/internal/structs/bitbucket_datacenter_integration.go b/spacelift/internal/structs/bitbucket_datacenter_integration.go index ff5c3559..deea1601 100644 --- a/spacelift/internal/structs/bitbucket_datacenter_integration.go +++ b/spacelift/internal/structs/bitbucket_datacenter_integration.go @@ -1,7 +1,5 @@ package structs -import "github.com/shurcooL/graphql" - // BitbucketDatacenterIntegration represents the bitbucket datacenter integration data relevant to the provider. type BitbucketDatacenterIntegration struct { ID string `graphql:"id"` @@ -18,20 +16,3 @@ type BitbucketDatacenterIntegration struct { WebhookSecret string `graphql:"webhookSecret"` WebhookURL string `graphql:"webhookURL"` } - -// CustomVCSInput represents the custom VCS input data. -type CustomVCSInput struct { - Name graphql.String `json:"name"` - SpaceID graphql.ID `json:"spaceID"` - Labels *[]graphql.String `json:"labels"` - Description *graphql.String `json:"description"` - IsDefault *graphql.Boolean `json:"isDefault"` -} - -// CustomVCSUpdateInput represents the custom VCS update input data. -type CustomVCSUpdateInput struct { - ID graphql.ID `json:"id"` - SpaceID graphql.ID `json:"space"` - Labels *[]graphql.String `json:"labels"` - Description *graphql.String `json:"description"` -} diff --git a/spacelift/internal/structs/gitlab_integration.go b/spacelift/internal/structs/gitlab_integration.go new file mode 100644 index 00000000..f74f72e1 --- /dev/null +++ b/spacelift/internal/structs/gitlab_integration.go @@ -0,0 +1,18 @@ +package structs + +// GitLabIntegration represents an GitLab identity provided by the Spacelift +// integration. +type GitLabIntegration struct { + ID string `graphql:"id"` + Name string `graphql:"name"` + Space struct { + ID string `graphql:"id"` + } `graphql:"space"` + IsDefault bool `graphql:"isDefault"` + Labels []string `graphql:"labels"` + Description *string `graphql:"description"` + APIHost string `graphql:"apiHost"` + UserFacingHost string `graphql:"userFacingHost"` + WebhookSecret string `graphql:"webhookSecret"` + WebhookURL string `graphql:"webhookUrl"` +} diff --git a/spacelift/internal/structs/vcs/input.go b/spacelift/internal/structs/vcs/input.go new file mode 100644 index 00000000..55dc52dc --- /dev/null +++ b/spacelift/internal/structs/vcs/input.go @@ -0,0 +1,12 @@ +package vcs + +import "github.com/shurcooL/graphql" + +// CustomVCSInput represents the custom VCS input data. +type CustomVCSInput struct { + Name graphql.String `json:"name"` + SpaceID graphql.ID `json:"spaceID"` + Labels *[]graphql.String `json:"labels"` + Description *graphql.String `json:"description"` + IsDefault *graphql.Boolean `json:"isDefault"` +} diff --git a/spacelift/internal/structs/vcs/update.go b/spacelift/internal/structs/vcs/update.go new file mode 100644 index 00000000..fb7ecf03 --- /dev/null +++ b/spacelift/internal/structs/vcs/update.go @@ -0,0 +1,11 @@ +package vcs + +import "github.com/shurcooL/graphql" + +// CustomVCSUpdateInput represents the custom VCS update input data. +type CustomVCSUpdateInput struct { + ID graphql.ID `json:"id"` + SpaceID graphql.ID `json:"space"` + Labels *[]graphql.String `json:"labels"` + Description *graphql.String `json:"description"` +} diff --git a/spacelift/provider.go b/spacelift/provider.go index 8f840e44..f67a3544 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -113,6 +113,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_drift_detection": resourceDriftDetection(), "spacelift_environment_variable": resourceEnvironmentVariable(), "spacelift_gcp_service_account": resourceGCPServiceAccount(), + "spacelift_gitlab_integration": resourceGitLabIntegration(), "spacelift_idp_group_mapping": resourceIdpGroupMapping(), "spacelift_module": resourceModule(), "spacelift_mounted_file": resourceMountedFile(), diff --git a/spacelift/resource_bitbucket_datacenter_integration.go b/spacelift/resource_bitbucket_datacenter_integration.go index 5521e9d7..ca862f9b 100644 --- a/spacelift/resource_bitbucket_datacenter_integration.go +++ b/spacelift/resource_bitbucket_datacenter_integration.go @@ -8,6 +8,7 @@ import ( "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs/vcs" "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" ) @@ -107,7 +108,7 @@ func resourceBitbucketDatacenterIntegrationCreate(ctx context.Context, d *schema } variables := map[string]interface{}{ - "customInput": &structs.CustomVCSInput{ + "customInput": &vcs.CustomVCSInput{ Name: toString(d.Get(bitbucketDatacenterName)), IsDefault: toOptionalBool(d.Get(bitbucketDatacenterIsDefault)), SpaceID: toString(d.Get(bitbucketDatacenterSpaceID)), @@ -158,7 +159,7 @@ func resourceBitbucketDatacenterIntegrationUpdate(ctx context.Context, d *schema "userFacingHost": toString(d.Get(bitbucketDatacenterUserFacingHost)), "username": toOptionalString(d.Get(bitbucketDatacenterUsername)), "accessToken": toOptionalString(d.Get(bitbucketDatacenterAccessToken)), - "customInput": &structs.CustomVCSUpdateInput{ + "customInput": &vcs.CustomVCSUpdateInput{ ID: toID(d.Id()), SpaceID: toString(d.Get(bitbucketDatacenterSpaceID)), Description: toOptionalString(d.Get(bitbucketDatacenterDescription)), diff --git a/spacelift/resource_gitlab_integration.go b/spacelift/resource_gitlab_integration.go new file mode 100644 index 00000000..6339b46a --- /dev/null +++ b/spacelift/resource_gitlab_integration.go @@ -0,0 +1,223 @@ +package spacelift + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs/vcs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" +) + +func resourceGitLabIntegration() *schema.Resource { + return &schema.Resource{ + Description: "`spacelift_gitlab_integration` represents an integration with a GitLab instance", + CreateContext: resourceGitLabIntegrationCreate, + ReadContext: resourceGitLabIntegrationRead, + UpdateContext: resourceGitLabIntegrationUpdate, + DeleteContext: resourceGitLabIntegrationDelete, + + CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if diff.HasChange(gitLabIsDefault) { + isDefault := diff.Get(gitLabIsDefault).(bool) + spaceID := diff.Get(gitLabSpaceID).(string) + if isDefault && spaceID != "root" { + return fmt.Errorf(`The default integration must be in the space "root" not in %q`, spaceID) + } + } + return nil + }, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + gitLabID: { + Type: schema.TypeString, + Description: "GitLab integration id.", + Computed: true, + }, + gitLabName: { + Type: schema.TypeString, + Description: "The friendly name of the integration", + Required: true, + ForceNew: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabDescription: { + Type: schema.TypeString, + Description: "Description of the integration", + Optional: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabAPIHost: { + Type: schema.TypeString, + Required: true, + Description: "API host URL", + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabUserFacingHost: { + Type: schema.TypeString, + Required: true, + Description: "User facing host URL.", + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabToken: { + Type: schema.TypeString, + Description: "The GitLab API Token", + Required: true, + Sensitive: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabLabels: { + Type: schema.TypeSet, + Description: "Labels to set on the integration", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + Optional: true, + }, + gitLabSpaceID: { + Type: schema.TypeString, + Description: "ID (slug) of the space the integration is in; Default: `root`", + Optional: true, + Default: "root", + ValidateDiagFunc: validations.DisallowEmptyString, + }, + gitLabIsDefault: { + Type: schema.TypeBool, + Description: "Is the GitLab integration the default for all spaces? If set to `true` the space must be set to `root` in `" + gitLabSpaceID + "` or left empty which uses the default", + Optional: true, + Default: false, + ForceNew: true, // unable to update isDefault flag + }, + gitLabWebhookSecret: { + Type: schema.TypeString, + Description: "Secret for webhooks originating from GitLab repositories", + Computed: true, + Sensitive: true, + }, + gitLabWebhookURL: { + Type: schema.TypeString, + Description: "URL for webhooks originating from GitLab repositories", + Computed: true, + }, + }, + } +} + +func resourceGitLabIntegrationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + CreateGitLabIntegration structs.GitLabIntegration `graphql:"gitlabIntegrationCreate(apiHost: $apiHost, userFacingHost: $userFacingHost, privateToken: $token, customInput: $customInput)"` + } + + variables := map[string]interface{}{ + "customInput": &vcs.CustomVCSInput{ + Name: toString(d.Get(gitLabName)), + IsDefault: toOptionalBool(d.Get(gitLabIsDefault)), + SpaceID: toString(d.Get(gitLabSpaceID)), + Labels: toOptionalStringList(d.Get(gitLabLabels)), + Description: toOptionalString(d.Get(gitLabDescription)), + }, + "apiHost": toString(d.Get(gitLabAPIHost)), + "userFacingHost": toString(d.Get(gitLabUserFacingHost)), + "token": toString(d.Get(gitLabToken)), + } + + if err := meta.(*internal.Client).Mutate(ctx, "GitLabIntegrationCreate", &mutation, variables); err != nil { + return diag.Errorf("could not create the GitLab integration: %v", internal.FromSpaceliftError(err)) + } + + fillGitLabIntegrationResults(d, &mutation.CreateGitLabIntegration) + + return nil +} + +func resourceGitLabIntegrationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var query struct { + GitLabIntegration *structs.GitLabIntegration `graphql:"gitlabIntegration(id: $id)"` + } + + variables := map[string]interface{}{"id": d.Id()} + if err := meta.(*internal.Client).Query(ctx, "GitLabIntegrationRead", &query, variables); err != nil { + return diag.Errorf("could not query for the gitlab integration: %v", err) + } + + if query.GitLabIntegration == nil { + d.SetId("") + } else { + fillGitLabIntegrationResults(d, query.GitLabIntegration) + } + + return nil +} + +func resourceGitLabIntegrationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + UpdateGitLabIntegration structs.GitLabIntegration `graphql:"gitlabIntegrationUpdate(apiHost: $apiHost, userFacingHost: $userFacingHost, privateToken: $privateToken, customInput: $customInput)"` + } + + variables := map[string]interface{}{ + "privateToken": toOptionalString(d.Get(gitLabToken)), + "apiHost": toString(d.Get(gitLabAPIHost)), + "userFacingHost": toString(d.Get(gitLabUserFacingHost)), + "customInput": &vcs.CustomVCSUpdateInput{ + ID: toID(d.Id()), + SpaceID: toString(d.Get(gitLabSpaceID)), + Description: toOptionalString(d.Get(gitLabDescription)), + Labels: toOptionalStringList(d.Get(gitLabLabels)), + }, + } + + var ret diag.Diagnostics + + if err := meta.(*internal.Client).Mutate(ctx, "GitLabIntegrationUpdate", &mutation, variables); err != nil { + ret = append(ret, diag.Errorf("could not update the GitLab integration: %v", internal.FromSpaceliftError(err))...) + } + + fillGitLabIntegrationResults(d, &mutation.UpdateGitLabIntegration) + + return ret +} + +func resourceGitLabIntegrationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var mutation struct { + DeleteGitLabIntegration *structs.GitLabIntegration `graphql:"gitlabIntegrationDelete(id: $id)"` + } + + variables := map[string]interface{}{ + "id": toID(d.Id()), + } + + if err := meta.(*internal.Client).Mutate(ctx, "GitLabIntegrationDelete", &mutation, variables); err != nil { + return diag.Errorf("could not delete GitLab integration: %v", internal.FromSpaceliftError(err)) + } + + d.SetId("") + + return nil +} + +func fillGitLabIntegrationResults(d *schema.ResourceData, gitLabIntegration *structs.GitLabIntegration) { + d.SetId(gitLabIntegration.ID) + d.Set(gitLabName, gitLabIntegration.Name) + d.Set(gitLabSpaceID, gitLabIntegration.Space.ID) + d.Set(gitLabIsDefault, gitLabIntegration.IsDefault) + d.Set(gitLabDescription, gitLabIntegration.Description) + d.Set(gitLabAPIHost, gitLabIntegration.APIHost) + d.Set(gitLabUserFacingHost, gitLabIntegration.UserFacingHost) + d.Set(gitLabWebhookURL, gitLabIntegration.WebhookURL) + d.Set(gitLabWebhookSecret, gitLabIntegration.WebhookSecret) + + labels := schema.NewSet(schema.HashString, []interface{}{}) + for _, label := range gitLabIntegration.Labels { + labels.Add(label) + } + d.Set(gitLabLabels, labels) +} diff --git a/spacelift/resource_gitlab_integration_test.go b/spacelift/resource_gitlab_integration_test.go new file mode 100644 index 00000000..eb09a37f --- /dev/null +++ b/spacelift/resource_gitlab_integration_test.go @@ -0,0 +1,195 @@ +package spacelift + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +func TestGitLabIntegrationResource(t *testing.T) { + const resourceName = "spacelift_gitlab_integration.test" + + t.Run("creates and updates a GitLab integration without an error", func(t *testing.T) { + random := func() string { return acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) } + + var ( + name = "my-test-gitlab-integration-" + random() + host = "https://gitlab.com/" + random() + token = "access-" + random() + descr = "description " + random() + labels = `["label1", "label2"]` + ) + + configGitLab := func(host, token, descr, labels string) string { + return ` + resource "spacelift_gitlab_integration" "test" { + name = "` + name + `" + api_host = "` + host + `" + user_facing_host = "` + host + `" + private_token = "` + token + `" + description = "` + descr + `" + labels = ` + labels + ` + } + ` + } + + configStack := func() string { + return ` + resource "spacelift_worker_pool" "test" { + name = "Let's create a dummy worker pool to avoid running the job ` + name + `" + space_id = "` + testConfig.SourceCode.Gitlab.SpaceLevel.Space + `" + } + + resource "spacelift_stack" "test" { + name = "stack-for-` + name + `" + repository = "` + testConfig.SourceCode.Gitlab.Repository.Name + `" + branch = "` + testConfig.SourceCode.Gitlab.Repository.Branch + `" + space_id = "` + testConfig.SourceCode.Gitlab.SpaceLevel.Space + `" + administrative = false + worker_pool_id = spacelift_worker_pool.test.id + gitlab { + namespace = "` + testConfig.SourceCode.Gitlab.Repository.Namespace + `" + id = spacelift_gitlab_integration.test.id + } + } + ` + } + + configRun := func() string { + return ` + resource "spacelift_run" "test" { + stack_id = spacelift_stack.test.id + keepers = { "branch" = "` + testConfig.SourceCode.Gitlab.Repository.Branch + `" } + } + ` + } + + spaceLevel := testConfig.SourceCode.Gitlab.SpaceLevel + + testSteps(t, []resource.TestStep{ + { + Config: configGitLab(host, token, descr, "null"), + Check: Resource( + resourceName, + Attribute(gitLabName, Equals(name)), + Attribute(gitLabAPIHost, Equals(host)), + Attribute(gitLabUserFacingHost, Equals(host)), + Attribute(gitLabToken, Equals(token)), + Attribute(gitLabWebhookURL, IsNotEmpty()), + Attribute(gitLabWebhookSecret, IsNotEmpty()), + Attribute(gitLabIsDefault, Equals("false")), + Attribute(gitLabDescription, Equals(descr)), + AttributeNotPresent(gitLabLabels), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{gitLabToken}, // specified only in the config + }, + { + Config: configGitLab(host, token, "new descr", `["new label1"]`), + Check: Resource( + resourceName, + Attribute(gitLabAPIHost, Equals(host)), + Attribute(gitLabUserFacingHost, Equals(host)), + Attribute(gitLabIsDefault, Equals("false")), + Attribute(gitLabDescription, Equals("new descr")), + Attribute(gitLabLabels+".#", Equals("1")), + Attribute(gitLabLabels+".0", Equals("new label1")), + ), + }, + { + Config: configGitLab(spaceLevel.APIHost, spaceLevel.Token, descr, labels), + Check: Resource( + resourceName, + Attribute(gitLabAPIHost, Equals(spaceLevel.APIHost)), + Attribute(gitLabUserFacingHost, Equals(spaceLevel.APIHost)), + Attribute(gitLabIsDefault, Equals("false")), + Attribute(gitLabToken, Equals(spaceLevel.Token)), + Attribute(gitLabDescription, Equals(descr)), + Attribute(gitLabLabels+".#", Equals("2")), + Attribute(gitLabLabels+".0", Equals("label1")), + Attribute(gitLabLabels+".1", Equals("label2")), + ), + }, + { + Config: configGitLab(spaceLevel.APIHost, spaceLevel.Token, descr, labels) + configStack(), + Check: Resource( + "spacelift_stack.test", + Attribute("gitlab.0.id", Equals(name)), + ), + }, + { + Config: configGitLab(spaceLevel.APIHost, spaceLevel.Token, descr, labels) + configStack() + configRun(), + Check: Resource( + "spacelift_run.test", + Attribute(gitLabID, IsNotEmpty()), + Attribute("stack_id", Equals("stack-for-"+name)), + ), + }, + }) + }) +} + +func TestGitLabIntegrationClearLabels(t *testing.T) { + const resourceName = "spacelift_gitlab_integration.test" + + t.Run("creates and updates a GitLab integration without an error", func(t *testing.T) { + random := func() string { return acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) } + + var ( + name = "my-test-gitlab-integration-" + random() + host = "https://gitlab.com/" + random() + token = "access-" + random() + descr = "description " + random() + ) + + configGitLab := func(host, token, descr, labels string) string { + return ` + resource "spacelift_gitlab_integration" "test" { + name = "` + name + `" + api_host = "` + host + `" + user_facing_host = "` + host + `" + private_token = "` + token + `" + description = "` + descr + `" + labels = ` + labels + ` + } + ` + } + testSteps(t, []resource.TestStep{ + { + Config: configGitLab(host, token, descr, "null"), + Check: Resource( + resourceName, + AttributeNotPresent(gitLabLabels), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{gitLabToken}, // specified only in the config + }, + { + Config: configGitLab(host, token, "new descr", `["new label1"]`), + Check: Resource( + resourceName, + Attribute(gitLabLabels+".#", Equals("1")), + Attribute(gitLabLabels+".0", Equals("new label1")), + ), + }, + { + Config: configGitLab(host, host, descr, "null"), + Check: Resource( + resourceName, + AttributeNotPresent(gitLabLabels), + ), + }, + }) + }) +}