Skip to content

Commit

Permalink
Merge pull request #226 from tosuke/framework-role
Browse files Browse the repository at this point in the history
Reimplement roles with tf-framework
  • Loading branch information
azukiazusa1 authored Aug 7, 2024
2 parents 780e445 + e1fff87 commit 9a863e6
Show file tree
Hide file tree
Showing 8 changed files with 569 additions and 0 deletions.
129 changes: 129 additions & 0 deletions internal/mackerel/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package mackerel

import (
"context"
"fmt"
"regexp"
"slices"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mackerelio/mackerel-client-go"
)

type RoleModel struct {
ID types.String `tfsdk:"id"`
ServiceName types.String `tfsdk:"service"`
RoleName types.String `tfsdk:"name"`
Memo types.String `tfsdk:"memo"`
}

func roleID(serviceName, roleName string) string {
return fmt.Sprintf("%s:%s", serviceName, roleName)
}

func parseRoleID(id string) (serviceName, roleName string, err error) {
serviceName, roleName, ok := strings.Cut(id, ":")
if !ok {
return "", "", fmt.Errorf("The ID is expected to have `<service>:<name>` format, but got: '%s'.", id)
}
return serviceName, roleName, nil
}

var roleNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_]+$`)

func RoleNameValidator() validator.String {
return stringvalidator.All(
stringvalidator.LengthBetween(2, 63),
stringvalidator.RegexMatches(
roleNameRegex,
"it can only contain letters, numbers, hyphens, and underscores and cannot start with a hyphen or underscore",
),
)
}

func ReadRole(ctx context.Context, client *Client, serviceName, roleName string) (RoleModel, error) {
return readRoleInner(ctx, client, serviceName, roleName)
}

type roleFinder interface {
FindRoles(string) ([]*mackerel.Role, error)
}

func readRoleInner(_ context.Context, client roleFinder, serviceName, roleName string) (RoleModel, error) {
roles, err := client.FindRoles(serviceName)
if err != nil {
return RoleModel{}, err
}

roleIdx := slices.IndexFunc(roles, func(r *mackerel.Role) bool {
return r.Name == roleName
})
if roleIdx < 0 {
return RoleModel{}, fmt.Errorf("the name '%s' does not match any role in mackerel.io", roleName)
}

role := roles[roleIdx]
return RoleModel{
ID: types.StringValue(roleID(serviceName, roleName)),
ServiceName: types.StringValue(serviceName),
RoleName: types.StringValue(roleName),
Memo: types.StringValue(role.Memo),
}, nil
}

func (m *RoleModel) Create(_ context.Context, client *Client) error {
serviceName := m.ServiceName.ValueString()
if _, err := client.CreateRole(serviceName, &mackerel.CreateRoleParam{
Name: m.RoleName.ValueString(),
Memo: m.Memo.ValueString(),
}); err != nil {
return err
}

m.ID = types.StringValue(roleID(serviceName, m.RoleName.ValueString()))

return nil
}

func (m *RoleModel) Read(ctx context.Context, client *Client) error {
return m.readInner(ctx, client)
}
func (m *RoleModel) readInner(ctx context.Context, client roleFinder) error {
// In ImportState, attributes other than `id` are unset.
var serviceName, roleName string
if !m.ID.IsNull() && !m.ID.IsUnknown() {
s, r, err := parseRoleID(m.ID.ValueString())
if err != nil {
return err
}
serviceName, roleName = s, r
} else {
serviceName = m.ServiceName.ValueString()
roleName = m.RoleName.ValueString()
}

r, err := readRoleInner(ctx, client, serviceName, roleName)
if err != nil {
return err
}

m.ID = r.ID // computed
m.ServiceName = r.ServiceName // required
m.RoleName = r.RoleName // required
m.Memo = r.Memo // has default

return nil
}

func (m *RoleModel) Delete(_ context.Context, client *Client) error {
if _, err := client.DeleteRole(
m.ServiceName.ValueString(),
m.RoleName.ValueString(),
); err != nil {
return err
}
return nil
}
158 changes: 158 additions & 0 deletions internal/mackerel/role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package mackerel

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mackerelio/mackerel-client-go"
)

func Test_Role_ReadRole(t *testing.T) {
t.Parallel()

defaultClient := func(service string) ([]*mackerel.Role, error) {
if service != "service0" {
return nil, fmt.Errorf("service not found")
}
return []*mackerel.Role{
{Name: "role0", Memo: "memo"},
{Name: "role1"},
}, nil
}

cases := map[string]struct {
inService string
inRole string
inClient roleFinderFunc

wants RoleModel
wantErr bool
}{
"valid": {
inService: "service0",
inRole: "role0",
inClient: defaultClient,

wants: RoleModel{
ID: types.StringValue("service0:role0"),
ServiceName: types.StringValue("service0"),
RoleName: types.StringValue("role0"),
Memo: types.StringValue("memo"),
},
},
"no role": {
inService: "service0",
inRole: "role-not-exists",
inClient: defaultClient,

wantErr: true,
},
}

ctx := context.Background()
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()

data, err := readRoleInner(ctx, tt.inClient, tt.inService, tt.inRole)
if err != nil {
if !tt.wantErr {
t.Errorf("unexpected error: %+v", err)
}
return
} else if tt.wantErr {
t.Error("expected error, but got no error")
return
}

if diff := cmp.Diff(data, tt.wants); diff != "" {
t.Error(diff)
}
})
}
}

func Test_RoleModel_Read(t *testing.T) {
t.Parallel()

defaultClient := roleFinderFunc(func(service string) ([]*mackerel.Role, error) {
if service != "service0" {
return nil, fmt.Errorf("service not found")
}
return []*mackerel.Role{
{Name: "role0", Memo: "memo"},
{Name: "role1"},
}, nil
})

cases := map[string]struct {
in RoleModel
inClient roleFinderFunc

wants RoleModel
wantErr bool
}{
"from id": {
in: RoleModel{ID: types.StringValue("service0:role0")},
inClient: defaultClient,

wants: RoleModel{
ID: types.StringValue("service0:role0"),
ServiceName: types.StringValue("service0"),
RoleName: types.StringValue("role0"),
Memo: types.StringValue("memo"),
},
},
"invalid id": {
in: RoleModel{ID: types.StringValue("invalid id")},
inClient: defaultClient,

wantErr: true,
},
"from service and name": {
in: RoleModel{
ServiceName: types.StringValue("service0"),
RoleName: types.StringValue("role0"),
},
inClient: defaultClient,

wants: RoleModel{
ID: types.StringValue("service0:role0"),
ServiceName: types.StringValue("service0"),
RoleName: types.StringValue("role0"),
Memo: types.StringValue("memo"),
},
},
}

ctx := context.Background()
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()

m := tt.in
if err := m.readInner(ctx, tt.inClient); err != nil {
if !tt.wantErr {
t.Errorf("unexpected error: %+v", err)
}
return
} else if tt.wantErr {
t.Error("expected error, but got no error")
return
}

if diff := cmp.Diff(m, tt.wants); diff != "" {
t.Error(diff)
}
})
}
}

type roleFinderFunc func(string) ([]*mackerel.Role, error)

func (f roleFinderFunc) FindRoles(serviceName string) ([]*mackerel.Role, error) {
return f(serviceName)
}
80 changes: 80 additions & 0 deletions internal/provider/data_source_mackerel_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package provider

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/mackerelio-labs/terraform-provider-mackerel/internal/mackerel"
)

var (
_ datasource.DataSource = (*mackerelRoleDataSource)(nil)
_ datasource.DataSourceWithConfigure = (*mackerelRoleDataSource)(nil)
)

func NewMackerelRoleDataSource() datasource.DataSource {
return &mackerelRoleDataSource{}
}

type mackerelRoleDataSource struct {
Client *mackerel.Client
}

func (d *mackerelRoleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_role"
}

func (d *mackerelRoleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "This data source allows access to details of a specific Role.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"service": schema.StringAttribute{
Description: "The name of the service.",
Required: true,
Validators: []validator.String{mackerel.ServiceNameValidator()},
},
"name": schema.StringAttribute{
Description: "The name of the role.",
Required: true,
Validators: []validator.String{mackerel.RoleNameValidator()},
},
"memo": schema.StringAttribute{
Description: "Notes related to this role.",
Computed: true,
},
},
}
}

func (d *mackerelRoleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
client, diags := retrieveClient(ctx, req.ProviderData)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
d.Client = client
}

func (d *mackerelRoleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config mackerel.RoleModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

data, err := mackerel.ReadRole(ctx, d.Client, config.ServiceName.ValueString(), config.RoleName.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unable to read Role",
err.Error(),
)
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
25 changes: 25 additions & 0 deletions internal/provider/data_source_mackerel_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package provider_test

import (
"context"
"testing"

fwdatasource "github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/mackerelio-labs/terraform-provider-mackerel/internal/provider"
)

func Test_MackerelRoleDataSource_schema(t *testing.T) {
t.Parallel()

ctx := context.Background()
req := fwdatasource.SchemaRequest{}
resp := fwdatasource.SchemaResponse{}
provider.NewMackerelRoleDataSource().Schema(ctx, req, &resp)
if resp.Diagnostics.HasError() {
t.Fatalf("schema: %+v", resp.Diagnostics)
}

if diags := resp.Schema.ValidateImplementation(ctx); diags.HasError() {
t.Fatalf("schema validation: %+v", diags)
}
}
Loading

0 comments on commit 9a863e6

Please sign in to comment.