feat: add user resource (#2)

This commit is contained in:
Dawid Wysokiński 2023-08-27 08:17:27 +02:00 committed by GitHub
parent b881a7ac23
commit 22cc289730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 355 additions and 35 deletions

View File

@ -6,6 +6,7 @@ GOBIN := $(shell go env GOPATH)/bin
endif
OSARCH=$(shell uname -m)
GOLANGCI_LINT_PATH=$(GOBIN)/golangci-lint
TFPLUGINDOCS_PATH=$(GOBIN)/tfplugindocs
.PHONY: install-git-hooks
install-git-hooks:
@ -15,8 +16,8 @@ install-git-hooks:
.PHONY: install-tfplugindocs
install-tfplugindocs:
@echo "Installing github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs..."
@go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.16.0
@echo "Installing github.com/hashicorp/terraform-plugin-docs..."
@(test -f $(TFPLUGINDOCS_PATH) && echo "github.com/hashicorp/terraform-plugin-docs is already installed. Skipping...") || (wget -q -O $(TFPLUGINDOCS_PATH).zip https://github.com/hashicorp/terraform-plugin-docs/releases/download/v0.16.0/tfplugindocs_0.16.0_$(GOOS)_$(GOARCH).zip && unzip $(TFPLUGINDOCS_PATH).zip tfplugindocs -d $(GOBIN) && rm $(TFPLUGINDOCS_PATH).zip)
.PHONY: install-golangci-lint
install-golangci-lint:

View File

@ -33,8 +33,8 @@ data "woodpecker_user" "user" {
### Read-Only
- `active` (Boolean) Whether user is active in the system
- `admin` (Boolean) Whether user is an admin
- `active` (Boolean) whether user is active in the system
- `admin` (Boolean) whether user is an admin
- `avatar` (String) the user's avatar URL
- `email` (String) the user's email
- `id` (Number) the user's id

43
docs/resources/user.md Normal file
View File

@ -0,0 +1,43 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "woodpecker_user Resource - terraform-provider-woodpecker"
subcategory: ""
description: |-
Provides a user resource.
This resource allows you to add/remove users. When applied, a new user will be created. When destroyed, that user will be removed.
---
# woodpecker_user (Resource)
Provides a user resource.
This resource allows you to add/remove users. When applied, a new user will be created. When destroyed, that user will be removed.
## Example Usage
```terraform
# Create a user
resource "woodpecker_user" "test" {
login = "test"
email = "test@localhost"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `login` (String) the name of the user
### Optional
- `admin` (Boolean) whether user is an admin
- `avatar` (String) the user's avatar URL
- `email` (String) the email of the user
### Read-Only
- `active` (Boolean) whether user is active in the system
- `id` (Number) the user's id

View File

@ -0,0 +1,5 @@
# Create a user
resource "woodpecker_user" "test" {
login = "test"
email = "test@localhost"
}

View File

@ -6,7 +6,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker"
)
@ -15,36 +14,18 @@ type userDataSource struct {
}
var _ datasource.DataSource = (*userDataSource)(nil)
var _ datasource.DataSourceWithConfigure = (*userDataSource)(nil)
func newUserDataSource() datasource.DataSource {
return &userDataSource{}
}
type userDataSourceModel struct {
ID types.Int64 `tfsdk:"id"`
Login types.String `tfsdk:"login"`
Email types.String `tfsdk:"email"`
Avatar types.String `tfsdk:"avatar"`
Active types.Bool `tfsdk:"active"`
Admin types.Bool `tfsdk:"admin"`
}
func (m *userDataSourceModel) setValues(user *woodpecker.User) {
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)
}
func (d *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user"
}
func (d *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Use this data source to retrieve information about a user.",
Attributes: map[string]schema.Attribute{
@ -66,17 +47,17 @@ func (d *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r
},
"active": schema.BoolAttribute{
Computed: true,
Description: "Whether user is active in the system",
Description: "whether user is active in the system",
},
"admin": schema.BoolAttribute{
Computed: true,
Description: "Whether user is an admin",
Description: "whether user is an admin",
},
},
}
}
func (d *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
func (d *userDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
@ -95,9 +76,8 @@ func (d *userDataSource) Configure(ctx context.Context, req datasource.Configure
}
func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data userDataSourceModel
var data userModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
@ -117,6 +97,5 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
data.setValues(user)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

35
internal/models.go Normal file
View File

@ -0,0 +1,35 @@
package internal
import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker"
)
type userModel struct {
ID types.Int64 `tfsdk:"id"`
Login types.String `tfsdk:"login"`
Email types.String `tfsdk:"email"`
Avatar types.String `tfsdk:"avatar"`
Active types.Bool `tfsdk:"active"`
Admin types.Bool `tfsdk:"admin"`
}
func (m *userModel) setValues(user *woodpecker.User) {
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)
}
func (m *userModel) toWoodpeckerModel() *woodpecker.User {
return &woodpecker.User{
ID: m.ID.ValueInt64(),
Login: m.Login.ValueString(),
Email: m.Email.ValueString(),
Avatar: m.Avatar.ValueString(),
Active: m.Active.ValueBool(),
Admin: m.Admin.ValueBool(),
}
}

View File

@ -15,7 +15,6 @@ import (
type woodpeckerProvider struct {
version string
client woodpecker.Client
}
var _ provider.Provider = (*woodpeckerProvider)(nil)
@ -60,7 +59,9 @@ func (p *woodpeckerProvider) DataSources(_ context.Context) []func() datasource.
}
func (p *woodpeckerProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{}
return []func() resource.Resource{
newUserResource,
}
}
func (p *woodpeckerProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
@ -69,10 +70,10 @@ func (p *woodpeckerProvider) Configure(ctx context.Context, req provider.Configu
return
}
p.client = newClient(ctx, cfg, resp)
client := newClient(ctx, cfg, resp)
resp.DataSourceData = p.client
resp.ResourceData = p.client
resp.DataSourceData = client
resp.ResourceData = client
}
type providerConfig struct {

163
internal/resource_user.go Normal file
View File

@ -0,0 +1,163 @@
package internal
import (
"context"
"fmt"
"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/woodpecker-ci/woodpecker/woodpecker-go/woodpecker"
)
type userResource struct {
client woodpecker.Client
}
var _ resource.Resource = (*userResource)(nil)
var _ resource.ResourceWithConfigure = (*userResource)(nil)
var _ resource.ResourceWithImportState = (*userResource)(nil)
func newUserResource() resource.Resource {
return &userResource{}
}
func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_user"
}
func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: `Provides a user resource.
This resource allows you to add/remove users. When applied, a new user will be created. When destroyed, that user will be removed.`,
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
Description: "the user's id",
},
"login": schema.StringAttribute{
Required: true,
Description: "the name of the user",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"email": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "the email of the user",
},
"avatar": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "the user's avatar URL",
},
"active": schema.BoolAttribute{
Computed: true,
Description: "whether user is active in the system",
},
"admin": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "whether user is an admin",
},
},
}
}
func (r *userResource) 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 *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data userModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
user, err := r.client.UserPost(data.toWoodpeckerModel())
if err != nil {
resp.Diagnostics.AddError("Couldn't create user", err.Error())
return
}
data.setValues(user)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data userModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
user, err := r.client.User(data.Login.ValueString())
if err != nil {
resp.Diagnostics.AddError("Couldn't get user", err.Error())
}
data.setValues(user)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data userModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
user, err := r.client.UserPatch(data.toWoodpeckerModel())
if err != nil {
resp.Diagnostics.AddError("Couldn't update user", err.Error())
return
}
data.setValues(user)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data userModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.UserDel(data.Login.ValueString()); err != nil {
resp.Diagnostics.AddError("Couldn't delete user", err.Error())
return
}
}
func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("login"), req.ID)...)
}

View File

@ -0,0 +1,93 @@
package internal_test
import (
"fmt"
"testing"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
)
func TestResourceUser(t *testing.T) {
t.Parallel()
login := uuid.NewString()
newLogin := uuid.NewString()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(`
resource "woodpecker_user" "test_user" {
login = "%s"
}
`, login),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", login),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", ""),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", ""),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "active", "false"),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "false"),
),
},
{
Config: fmt.Sprintf(`
resource "woodpecker_user" "test_user" {
login = "%s"
email = "%s@localhost"
avatar = "http://localhost/%s"
admin = true
}
`, login, login, login),
Check: resource.ComposeAggregateTestCheckFunc(
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),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "active", "false"),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "true"),
),
},
{
Config: fmt.Sprintf(`
resource "woodpecker_user" "test_user" {
login = "%s"
}
`, login),
Check: resource.ComposeAggregateTestCheckFunc(
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),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "active", "false"),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "true"),
),
},
{
ResourceName: "woodpecker_user.test_user",
ImportState: true,
ImportStateId: login,
ImportStateVerify: true,
},
{
Config: fmt.Sprintf(`
resource "woodpecker_user" "test_user" {
login = "%s"
}
`, newLogin),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("woodpecker_user.test_user", plancheck.ResourceActionReplace),
},
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("woodpecker_user.test_user", "login", newLogin),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "email", ""),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "avatar", ""),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "active", "false"),
resource.TestCheckResourceAttr("woodpecker_user.test_user", "admin", "false"),
),
},
},
})
}