-
Notifications
You must be signed in to change notification settings - Fork 15
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 #214 from tosuke/framework-mackerel-service
Implement service resource and data source in the framework provider
- Loading branch information
Showing
16 changed files
with
789 additions
and
108 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
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
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,66 @@ | ||
package mackerel | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
"os" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" | ||
"github.com/mackerelio/mackerel-client-go" | ||
) | ||
|
||
type Client = mackerel.Client | ||
|
||
type ClientConfigModel struct { | ||
APIKey types.String `tfsdk:"api_key"` | ||
APIBase types.String `tfsdk:"api_base"` | ||
} | ||
|
||
var ( | ||
ErrNoAPIKey = errors.New("API Key for Mackerel is not found.") | ||
) | ||
|
||
func NewClientConfigFromEnv() ClientConfigModel { | ||
var data ClientConfigModel | ||
|
||
var apiKey string | ||
for _, env := range []string{"MACKEREL_APIKEY", "MACKEREL_API_KEY"} { | ||
if apiKey == "" { | ||
apiKey = os.Getenv(env) | ||
} | ||
} | ||
if apiKey != "" { | ||
data.APIKey = types.StringValue(apiKey) | ||
} | ||
|
||
apiBase := os.Getenv("API_BASE") | ||
if apiBase != "" { | ||
data.APIBase = types.StringValue(apiBase) | ||
} | ||
|
||
return data | ||
} | ||
|
||
func (m *ClientConfigModel) NewClient() (*Client, error) { | ||
apiKey := m.APIKey.ValueString() | ||
if apiKey == "" { | ||
return nil, ErrNoAPIKey | ||
} | ||
|
||
apiBase := m.APIBase.ValueString() | ||
|
||
var client *mackerel.Client | ||
if apiBase == "" { | ||
client = mackerel.NewClient(apiKey) | ||
} else { | ||
// TODO: use logging transport with tflog (FYI: https://github.com/hashicorp/terraform-plugin-log/issues/91) | ||
c, err := mackerel.NewClientWithOptions(apiKey, apiBase, false) | ||
if err != nil { | ||
return nil, err | ||
} | ||
client = c | ||
} | ||
client.HTTPClient.Transport = logging.NewSubsystemLoggingHTTPTransport("Mackerel", http.DefaultTransport) | ||
return client, 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,57 @@ | ||
package mackerel | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
func Test_ClientConfig_apiKeyCompat(t *testing.T) { | ||
cases := map[string]struct { | ||
MACKEREL_APIKEY string | ||
MACKEREL_API_KEY string | ||
want types.String | ||
}{ | ||
"no key": { | ||
want: types.StringNull(), | ||
}, | ||
"MACKEREL_API_KEY": { | ||
MACKEREL_API_KEY: "api_key", | ||
|
||
want: types.StringValue("api_key"), | ||
}, | ||
"MACKEREL_APIKEY": { | ||
MACKEREL_APIKEY: "apikey", | ||
|
||
want: types.StringValue("apikey"), | ||
}, | ||
"both": { | ||
MACKEREL_APIKEY: "apikey", | ||
MACKEREL_API_KEY: "api_key", | ||
|
||
want: types.StringValue("apikey"), | ||
}, | ||
} | ||
|
||
for name, tt := range cases { | ||
t.Run(name, func(t *testing.T) { | ||
t.Setenv("MACKEREL_APIKEY", tt.MACKEREL_APIKEY) | ||
t.Setenv("MACKEREL_API_KEY", tt.MACKEREL_API_KEY) | ||
|
||
c := NewClientConfigFromEnv() | ||
if c.APIKey != tt.want { | ||
t.Errorf("expected to be '%s', but got '%s'.", tt.want, c.APIKey) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func Test_ClientConfig_noApiKey(t *testing.T) { | ||
t.Parallel() | ||
|
||
var config ClientConfigModel | ||
if _, err := config.NewClient(); !errors.Is(err, ErrNoAPIKey) { | ||
t.Errorf("expected to ErrNoAPIKey, but got: %v", err) | ||
} | ||
} |
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,92 @@ | ||
package mackerel | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"regexp" | ||
"slices" | ||
|
||
"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" | ||
) | ||
|
||
var serviceNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_]+$`) | ||
|
||
func ServiceNameValidator() validator.String { | ||
return stringvalidator.All( | ||
stringvalidator.LengthBetween(2, 63), | ||
stringvalidator.RegexMatches(serviceNameRegex, | ||
"Must include only alphabets, numbers, hyphen and underscore, and it can not begin a hyphen or underscore"), | ||
) | ||
} | ||
|
||
type ServiceModel struct { | ||
ID types.String `tfsdk:"id"` | ||
Name types.String `tfsdk:"name"` | ||
Memo types.String `tfsdk:"memo"` | ||
} | ||
|
||
func ReadService(ctx context.Context, client *Client, name string) (*ServiceModel, error) { | ||
return readServiceInner(ctx, client, name) | ||
} | ||
|
||
type serviceFinder interface { | ||
FindServices() ([]*mackerel.Service, error) | ||
} | ||
|
||
func readServiceInner(_ context.Context, client serviceFinder, name string) (*ServiceModel, error) { | ||
services, err := client.FindServices() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
serviceIdx := slices.IndexFunc(services, func(s *mackerel.Service) bool { | ||
return s.Name == name | ||
}) | ||
if serviceIdx == -1 { | ||
return nil, fmt.Errorf("the name '%s' does not match any service in mackerel.io", name) | ||
} | ||
|
||
service := services[serviceIdx] | ||
return &ServiceModel{ | ||
ID: types.StringValue(service.Name), | ||
Name: types.StringValue(service.Name), | ||
Memo: types.StringValue(service.Memo), | ||
}, nil | ||
} | ||
|
||
func (m *ServiceModel) Set(newData ServiceModel) { | ||
if !newData.ID.IsUnknown() { | ||
m.ID = newData.ID | ||
} | ||
m.Name = newData.Name | ||
|
||
// If Memo is an empty string, treat it as null | ||
if newData.Memo.ValueString() != "" { | ||
m.Memo = newData.Memo | ||
} | ||
} | ||
|
||
func (m *ServiceModel) Create(_ context.Context, client *Client) error { | ||
param := mackerel.CreateServiceParam{ | ||
Name: m.Name.ValueString(), | ||
Memo: m.Memo.ValueString(), | ||
} | ||
|
||
service, err := client.CreateService(¶m) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
m.ID = types.StringValue(service.Name) | ||
return nil | ||
} | ||
|
||
func (m *ServiceModel) Delete(_ context.Context, client *Client) error { | ||
if _, err := client.DeleteService(m.ID.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,134 @@ | ||
package mackerel | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/mackerelio/mackerel-client-go" | ||
) | ||
|
||
func Test_ServiceNameValidator(t *testing.T) { | ||
t.Parallel() | ||
|
||
cases := map[string]struct { | ||
val types.String | ||
wantError bool | ||
}{ | ||
"valid": { | ||
val: types.StringValue("service1"), | ||
}, | ||
"too short": { | ||
val: types.StringValue("a"), | ||
wantError: true, | ||
}, | ||
"too long": { | ||
val: types.StringValue("toooooooooooooooooooo-looooooooooooooooooooooooooooooooooooooooooong"), | ||
wantError: true, | ||
}, | ||
"invalid char": { | ||
val: types.StringValue("v('ω')v"), | ||
wantError: true, | ||
}, | ||
} | ||
|
||
ctx := context.Background() | ||
for name, tt := range cases { | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
req := validator.StringRequest{ | ||
Path: path.Root("test"), | ||
PathExpression: path.MatchRoot("test"), | ||
ConfigValue: tt.val, | ||
} | ||
resp := &validator.StringResponse{} | ||
ServiceNameValidator().ValidateString(ctx, req, resp) | ||
|
||
hasError := resp.Diagnostics.HasError() | ||
if hasError != tt.wantError { | ||
if tt.wantError { | ||
t.Error("expected to have errors, but got no error") | ||
} else { | ||
t.Errorf("unexpected error: %+v", resp.Diagnostics.Errors()) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
type serviceFinderFunc func() ([]*mackerel.Service, error) | ||
|
||
func (f serviceFinderFunc) FindServices() ([]*mackerel.Service, error) { | ||
return f() | ||
} | ||
|
||
func Test_ReadService(t *testing.T) { | ||
t.Parallel() | ||
|
||
cases := map[string]struct { | ||
inClient serviceFinderFunc | ||
inName string | ||
want *ServiceModel | ||
wantFail bool | ||
}{ | ||
"success": { | ||
inClient: func() ([]*mackerel.Service, error) { | ||
return []*mackerel.Service{ | ||
{ | ||
Name: "service0", | ||
}, | ||
{ | ||
Name: "service1", | ||
Memo: "memo", | ||
}, | ||
}, nil | ||
}, | ||
inName: "service1", | ||
want: &ServiceModel{ | ||
ID: types.StringValue("service1"), | ||
Name: types.StringValue("service1"), | ||
Memo: types.StringValue("memo"), | ||
}, | ||
}, | ||
"no service": { | ||
inClient: func() ([]*mackerel.Service, error) { | ||
return []*mackerel.Service{ | ||
{ | ||
Name: "service0", | ||
}, | ||
{ | ||
Name: "service1", | ||
Memo: "memo", | ||
}, | ||
}, nil | ||
}, | ||
inName: "service2", | ||
wantFail: true, | ||
}, | ||
} | ||
|
||
ctx := context.Background() | ||
for name, tt := range cases { | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
s, err := readServiceInner(ctx, tt.inClient, tt.inName) | ||
if err != nil { | ||
if !tt.wantFail { | ||
t.Errorf("unexpected error: %+v", err) | ||
} | ||
return | ||
} | ||
if tt.wantFail { | ||
t.Errorf("unexpected success") | ||
} | ||
|
||
if diff := cmp.Diff(tt.want, s); diff != "" { | ||
t.Errorf("%s", diff) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.