Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement service resource and data source in the framework provider #214

Merged
merged 12 commits into from
Jun 20, 2024
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"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so cute lol

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
Loading