diff --git a/docs/resources/secret.md b/docs/resources/secret.md new file mode 100644 index 0000000..a40eaa2 --- /dev/null +++ b/docs/resources/secret.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "woodpecker_secret Resource - terraform-provider-woodpecker" +subcategory: "" +description: |- + This resource allows you to add/remove global secrets. When applied, a new secret will be created. When destroyed, that secret will be removed. For more information see the Woodpecker docs https://woodpecker-ci.org/docs/usage/secrets. +--- + +# woodpecker_secret (Resource) + +This resource allows you to add/remove global secrets. When applied, a new secret will be created. When destroyed, that secret will be removed. For more information see [the Woodpecker docs](https://woodpecker-ci.org/docs/usage/secrets). + +## Example Usage + +```terraform +# Create a secret +resource "woodpecker_secret" "test" { + name = "test" + value = "test" + events = ["cron", "deployment"] +} +``` + + +## Schema + +### Required + +- `events` (Set of String) events for which the secret is available (push, tag, pull_request, deployment, cron, manual) +- `name` (String) the name of the secret +- `value` (String, Sensitive) the value of the secret + +### Optional + +- `images` (Set of String) list of Docker images for which this secret is available, leave blank to allow all images +- `plugins_only` (Boolean) whether secret is only available for [plugins](https://woodpecker-ci.org/docs/usage/plugins/plugins) + +### Read-Only + +- `id` (Number) the secret's id diff --git a/examples/resources/woodpecker_secret/resource.tf b/examples/resources/woodpecker_secret/resource.tf new file mode 100644 index 0000000..c142e9d --- /dev/null +++ b/examples/resources/woodpecker_secret/resource.tf @@ -0,0 +1,6 @@ +# Create a secret +resource "woodpecker_secret" "test" { + name = "test" + value = "test" + events = ["cron", "deployment"] +} diff --git a/go.mod b/go.mod index 730a5ca..9213e3c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( code.gitea.io/sdk/gitea v0.15.1-0.20230815151548-091528835fc2 github.com/google/uuid v1.3.1 github.com/hashicorp/terraform-plugin-framework v1.3.5 + github.com/hashicorp/terraform-plugin-framework-validators v0.11.0 github.com/hashicorp/terraform-plugin-go v0.18.0 github.com/hashicorp/terraform-plugin-testing v1.4.0 github.com/ory/dockertest/v3 v3.10.0 diff --git a/go.sum b/go.sum index 6b47d8c..2e72744 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQH github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= github.com/hashicorp/terraform-plugin-framework v1.3.5 h1:FJ6s3CVWVAxlhiF/jhy6hzs4AnPHiflsp9KgzTGl1wo= github.com/hashicorp/terraform-plugin-framework v1.3.5/go.mod h1:2gGDpWiTI0irr9NSTLFAKlTi6KwGti3AoU19rFqU30o= +github.com/hashicorp/terraform-plugin-framework-validators v0.11.0 h1:DKb1bX7/EPZUTW6F5zdwJzS/EZ/ycVD6JAW5RYOj4f8= +github.com/hashicorp/terraform-plugin-framework-validators v0.11.0/go.mod h1:dzxOiHh7O9CAwc6p8N4mR1H++LtRkl+u+21YNiBVNno= github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/internal/data_source_user.go b/internal/data_source_user.go index cc59b46..fb9c10b 100644 --- a/internal/data_source_user.go +++ b/internal/data_source_user.go @@ -94,7 +94,10 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - data.setValues(user) + resp.Diagnostics.Append(data.setValues(ctx, user)...) + if resp.Diagnostics.HasError() { + return + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/models.go b/internal/models.go index 416d0fd..40bfd7e 100644 --- a/internal/models.go +++ b/internal/models.go @@ -1,6 +1,9 @@ package internal import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker" ) @@ -14,16 +17,17 @@ type userModel struct { Admin types.Bool `tfsdk:"admin"` } -func (m *userModel) setValues(user *woodpecker.User) { +func (m *userModel) setValues(_ context.Context, user *woodpecker.User) diag.Diagnostics { m.ID = types.Int64Value(user.ID) m.Login = types.StringValue(user.Login) m.Email = types.StringValue(user.Email) m.Avatar = types.StringValue(user.Avatar) m.Active = types.BoolValue(user.Active) m.Admin = types.BoolValue(user.Admin) + return nil } -func (m *userModel) toWoodpeckerModel() *woodpecker.User { +func (m *userModel) toWoodpeckerModel(_ context.Context) (*woodpecker.User, diag.Diagnostics) { return &woodpecker.User{ ID: m.ID.ValueInt64(), Login: m.Login.ValueString(), @@ -31,5 +35,44 @@ func (m *userModel) toWoodpeckerModel() *woodpecker.User { Avatar: m.Avatar.ValueString(), Active: m.Active.ValueBool(), Admin: m.Admin.ValueBool(), - } + }, nil +} + +type secretModel struct { + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` + Images types.Set `tfsdk:"images"` + PluginsOnly types.Bool `tfsdk:"plugins_only"` + Events types.Set `tfsdk:"events"` +} + +func (m *secretModel) setValues(ctx context.Context, secret *woodpecker.Secret) diag.Diagnostics { + var diagsRes diag.Diagnostics + var diags diag.Diagnostics + + m.ID = types.Int64Value(secret.ID) + m.Name = types.StringValue(secret.Name) + m.Images, diags = types.SetValueFrom(ctx, types.StringType, secret.Images) + diagsRes.Append(diags...) + m.PluginsOnly = types.BoolValue(secret.PluginsOnly) + m.Events, diags = types.SetValueFrom(ctx, types.StringType, secret.Events) + diagsRes.Append(diags...) + + return diagsRes +} + +func (m *secretModel) toWoodpeckerModel(ctx context.Context) (*woodpecker.Secret, diag.Diagnostics) { + var diags diag.Diagnostics + + secret := &woodpecker.Secret{ + ID: m.ID.ValueInt64(), + Name: m.Name.ValueString(), + Value: m.Value.ValueString(), + PluginsOnly: m.PluginsOnly.ValueBool(), + } + diags.Append(m.Images.ElementsAs(ctx, &secret.Images, false)...) + diags.Append(m.Events.ElementsAs(ctx, &secret.Events, false)...) + + return secret, diags } diff --git a/internal/provider.go b/internal/provider.go index 33e9052..3205571 100644 --- a/internal/provider.go +++ b/internal/provider.go @@ -61,6 +61,7 @@ func (p *woodpeckerProvider) DataSources(_ context.Context) []func() datasource. func (p *woodpeckerProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ newUserResource, + newSecretResource, } } diff --git a/internal/resource_secret.go b/internal/resource_secret.go new file mode 100644 index 0000000..41648fa --- /dev/null +++ b/internal/resource_secret.go @@ -0,0 +1,195 @@ +package internal + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker" +) + +type secretResource struct { + client woodpecker.Client +} + +var _ resource.Resource = (*secretResource)(nil) +var _ resource.ResourceWithConfigure = (*secretResource)(nil) +var _ resource.ResourceWithImportState = (*secretResource)(nil) + +func newSecretResource() resource.Resource { + return &secretResource{} +} + +func (r *secretResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_secret" +} + +func (r *secretResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "This resource allows you to add/remove global secrets. When applied, a new secret will be created." + + " When destroyed, that secret will be removed." + + " For more information see [the Woodpecker docs](https://woodpecker-ci.org/docs/usage/secrets).", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + Description: "the secret's id", + }, + "name": schema.StringAttribute{ + Required: true, + Description: "the name of the secret", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "value": schema.StringAttribute{ + Required: true, + Description: "the value of the secret", + Sensitive: true, + }, + "events": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "events for which the secret is available (push, tag, pull_request, deployment, cron, manual)", + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOfCaseInsensitive("push", "tag", "pull_request", "deployment", "cron", "manual"), + ), + }, + }, + "plugins_only": schema.BoolAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "whether secret is only available for [plugins](https://woodpecker-ci.org/docs/usage/plugins/plugins)", + }, + "images": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Description: "list of Docker images for which this secret is available, leave blank to allow all images", + }, + }, + } +} + +func (r *secretResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(woodpecker.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected woodpecker.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *secretResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data secretModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + wData, diags := data.toWoodpeckerModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + secret, err := r.client.GlobalSecretCreate(wData) + if err != nil { + resp.Diagnostics.AddError("Couldn't create secret", err.Error()) + return + } + + resp.Diagnostics.Append(data.setValues(ctx, secret)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *secretResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data secretModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + secret, err := r.client.GlobalSecret(data.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Couldn't get secret", err.Error()) + } + + resp.Diagnostics.Append(data.setValues(ctx, secret)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *secretResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data secretModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + wData, diags := data.toWoodpeckerModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + secret, err := r.client.GlobalSecretUpdate(wData) + if err != nil { + resp.Diagnostics.AddError("Couldn't update secret", err.Error()) + return + } + + resp.Diagnostics.Append(data.setValues(ctx, secret)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *secretResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data secretModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.client.GlobalSecretDelete(data.Name.ValueString()); err != nil { + resp.Diagnostics.AddError("Couldn't delete secret", err.Error()) + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *secretResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), req.ID)...) +} diff --git a/internal/resource_secret_test.go b/internal/resource_secret_test.go new file mode 100644 index 0000000..6710107 --- /dev/null +++ b/internal/resource_secret_test.go @@ -0,0 +1,154 @@ +package internal_test + +import ( + "errors" + "fmt" + "regexp" + "slices" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker" +) + +func TestResourceSecret(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + name := uuid.NewString() + newName := uuid.NewString() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckSecretResourceDestroy(name, newName), + Steps: []resource.TestStep{ + { // create secret + Config: fmt.Sprintf(` +resource "woodpecker_secret" "test_secret" { + name = "%s" + value = "test123" + events = ["push"] +} +`, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_secret.test_secret", "id"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "name", name), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "value", "test123"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "push"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "plugins_only", "false"), + ), + }, + { // update secret + Config: fmt.Sprintf(` +resource "woodpecker_secret" "test_secret" { + name = "%s" + value = "test123123" + events = ["push", "deployment"] + plugins_only = true + images = ["testimage"] +} +`, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_secret.test_secret", "id"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "name", name), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "value", "test123123"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "push"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "deployment"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "plugins_only", "true"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "images.*", "testimage"), + ), + }, + { // fields shouldn't be overridden + Config: fmt.Sprintf(` +resource "woodpecker_secret" "test_secret" { + name = "%s" + value = "test123123" + events = ["push", "deployment"] +} +//`, name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_secret.test_secret", "id"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "name", name), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "value", "test123123"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "push"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "deployment"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "plugins_only", "true"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "images.*", "testimage"), + ), + }, + { // import + ResourceName: "woodpecker_secret.test_secret", + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, + { // replace secret + Config: fmt.Sprintf(` +resource "woodpecker_secret" "test_secret" { + name = "%s" + value = "test123New" + events = ["push"] +} +`, newName), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("woodpecker_secret.test_secret", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_secret.test_secret", "id"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "name", newName), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "value", "test123New"), + resource.TestCheckTypeSetElemAttr("woodpecker_secret.test_secret", "events.*", "push"), + resource.TestCheckResourceAttr("woodpecker_secret.test_secret", "plugins_only", "false"), + ), + }, + }, + }) + }) + + t.Run("ERR: incorrect event value", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { // create secret + Config: fmt.Sprintf(` +resource "woodpecker_secret" "test_secret" { + name = "%s" + value = "test123" + events = ["random"] +} +`, uuid.NewString()), + ExpectError: regexp.MustCompile(`Attribute events\[Value\("random"\)] value must be one of`), + }, + }, + }) + }) +} + +func testAccCheckSecretResourceDestroy(names ...string) func(state *terraform.State) error { + return func(state *terraform.State) error { + secrets, err := woodpeckerClient.GlobalSecretList() + if err != nil { + return fmt.Errorf("couldn't list secrets: %w", err) + } + + if slices.ContainsFunc(secrets, func(secret *woodpecker.Secret) bool { + return slices.Contains(names, secret.Name) + }) { + return errors.New("at least one of the created secrets isn't deleted") + } + + return nil + } +} diff --git a/internal/resource_user.go b/internal/resource_user.go index 0e21e30..abe390b 100644 --- a/internal/resource_user.go +++ b/internal/resource_user.go @@ -95,13 +95,22 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r return } - user, err := r.client.UserPost(data.toWoodpeckerModel()) + wData, diags := data.toWoodpeckerModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user, err := r.client.UserPost(wData) if err != nil { resp.Diagnostics.AddError("Couldn't create user", err.Error()) return } - data.setValues(user) + resp.Diagnostics.Append(data.setValues(ctx, user)...) + if resp.Diagnostics.HasError() { + return + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -119,7 +128,10 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.Diagnostics.AddError("Couldn't get user", err.Error()) } - data.setValues(user) + resp.Diagnostics.Append(data.setValues(ctx, user)...) + if resp.Diagnostics.HasError() { + return + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -132,13 +144,22 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - user, err := r.client.UserPatch(data.toWoodpeckerModel()) + wData, diags := data.toWoodpeckerModel(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + user, err := r.client.UserPatch(wData) if err != nil { resp.Diagnostics.AddError("Couldn't update user", err.Error()) return } - data.setValues(user) + resp.Diagnostics.Append(data.setValues(ctx, user)...) + if resp.Diagnostics.HasError() { + return + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -155,6 +176,10 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r resp.Diagnostics.AddError("Couldn't delete user", err.Error()) return } + + // If execution completes without error, the framework will automatically + // call DeleteResponse.State.RemoveResource(), so it can be omitted + // from provider logic. } func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/resource_user_test.go b/internal/resource_user_test.go index 3e2f18c..be93567 100644 --- a/internal/resource_user_test.go +++ b/internal/resource_user_test.go @@ -1,12 +1,16 @@ package internal_test import ( + "errors" "fmt" + "slices" "testing" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker" ) func TestResourceUser(t *testing.T) { @@ -14,17 +18,20 @@ func TestResourceUser(t *testing.T) { login := uuid.NewString() newLogin := uuid.NewString() + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckUserResourceDestroy(login, newLogin), Steps: []resource.TestStep{ - { + { // create user Config: fmt.Sprintf(` resource "woodpecker_user" "test_user" { login = "%s" } `, login), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_user.test_user", "id"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", login), resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", ""), resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", ""), @@ -32,7 +39,7 @@ resource "woodpecker_user" "test_user" { resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "false"), ), }, - { + { // update user Config: fmt.Sprintf(` resource "woodpecker_user" "test_user" { login = "%s" @@ -42,6 +49,7 @@ resource "woodpecker_user" "test_user" { } `, login, login, login), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_user.test_user", "id"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", login), resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", login+"@localhost"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", "http://localhost/"+login), @@ -49,13 +57,14 @@ resource "woodpecker_user" "test_user" { resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "true"), ), }, - { + { // fields shouldn't be overridden Config: fmt.Sprintf(` resource "woodpecker_user" "test_user" { login = "%s" } `, login), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_user.test_user", "id"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", login), resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", login+"@localhost"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", "http://localhost/"+login), @@ -63,13 +72,13 @@ resource "woodpecker_user" "test_user" { resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "true"), ), }, - { + { // import ResourceName: "woodpecker_user.test_user", ImportState: true, ImportStateId: login, ImportStateVerify: true, }, - { + { // replace user Config: fmt.Sprintf(` resource "woodpecker_user" "test_user" { login = "%s" @@ -81,6 +90,7 @@ resource "woodpecker_user" "test_user" { }, }, Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("woodpecker_user.test_user", "id"), resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", newLogin), resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", ""), resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", ""), @@ -91,3 +101,20 @@ resource "woodpecker_user" "test_user" { }, }) } + +func testAccCheckUserResourceDestroy(logins ...string) func(state *terraform.State) error { + return func(state *terraform.State) error { + users, err := woodpeckerClient.UserList() + if err != nil { + return fmt.Errorf("couldn't list users: %w", err) + } + + if slices.ContainsFunc(users, func(user *woodpecker.User) bool { + return slices.Contains(logins, user.Login) + }) { + return errors.New("at least one of the created users isn't deleted") + } + + return nil + } +}