-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #226 from tosuke/framework-role
Reimplement roles with tf-framework
- Loading branch information
Showing
8 changed files
with
569 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.