Skip to content

Commit

Permalink
Merge pull request #214 from tosuke/framework-mackerel-service
Browse files Browse the repository at this point in the history
Implement service resource and data source in the framework provider
  • Loading branch information
Arthur1 committed Jun 20, 2024
2 parents 73005a2 + 225479b commit 8efe2a4
Show file tree
Hide file tree
Showing 16 changed files with 789 additions and 108 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ go 1.22.0

require (
github.com/golangci/golangci-lint v1.50.1
github.com/google/go-cmp v0.6.0
github.com/hashicorp/terraform-plugin-framework v1.8.0
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-mux v0.16.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
Expand Down Expand Up @@ -71,7 +73,6 @@ require (
github.com/golangci/misspell v0.3.5 // indirect
github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-framework v1.8.0 h1:P07qy8RKLcoBkCrY2RHJer5AEvJnDuXomBgou6fD8kI=
github.com/hashicorp/terraform-plugin-framework v1.8.0/go.mod h1:/CpTukO88PcL/62noU7cuyaSJ4Rsim+A/pa+3rUVufY=
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
Expand Down
66 changes: 66 additions & 0 deletions internal/mackerel/config.go
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
}
57 changes: 57 additions & 0 deletions internal/mackerel/config_test.go
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)
}
}
92 changes: 92 additions & 0 deletions internal/mackerel/service.go
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(&param)
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
}
134 changes: 134 additions & 0 deletions internal/mackerel/service_test.go
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)
}
})
}
}
Loading

0 comments on commit 8efe2a4

Please sign in to comment.