Skip to content

Commit

Permalink
Merge pull request #219 from tosuke/framework-service-metadata
Browse files Browse the repository at this point in the history
Reimplement service metadata resources and data sources with tf-framework
  • Loading branch information
Arthur1 authored Jun 30, 2024
2 parents 8efe2a4 + 8792671 commit 4fcbdf4
Show file tree
Hide file tree
Showing 10 changed files with 650 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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-jsontypes v0.1.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
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-jsontypes v0.1.0 h1:b8vZYB/SkXJT4YPbT3trzE6oJ7dPyMy68+9dEDKsJjE=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0/go.mod h1:tP9BC3icoXBz72evMS5UTFvi98CiKhPdXF6yLs1wS8A=
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=
Expand Down
149 changes: 149 additions & 0 deletions internal/mackerel/service_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package mackerel

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mackerelio/mackerel-client-go"
)

type ServiceMetadataModel struct {
ID types.String `tfsdk:"id"`
ServiceName types.String `tfsdk:"service"`
Namespace types.String `tfsdk:"namespace"`
MetadataJSON jsontypes.Normalized `tfsdk:"metadata_json"`
}

func serviceMetadataID(serviceName, namespace string) string {
return strings.Join([]string{serviceName, namespace}, "/")
}

func parseServiceMetadataID(id string) (serviceName, namespace string, err error) {
first, last, ok := strings.Cut(id, "/")
if !ok {
return "", "", fmt.Errorf("The ID is expected to have `<service_name>/<namespace>` format, but got: '%s'.", id)
}
return first, last, nil
}

func ReadServiceMetadata(ctx context.Context, client *Client, data ServiceMetadataModel) (ServiceMetadataModel, error) {
return readServiceMetadataInner(ctx, client, data)
}

type serviceMetadataGetter interface {
GetServiceMetaData(string, string) (*mackerel.ServiceMetaDataResp, error)
}

func readServiceMetadataInner(_ context.Context, client serviceMetadataGetter, data ServiceMetadataModel) (ServiceMetadataModel, error) {
serviceName, namespace, err := data.getID()
if err != nil {
return ServiceMetadataModel{}, err
}

metadataResp, err := client.GetServiceMetaData(serviceName, namespace)
if err != nil {
return ServiceMetadataModel{}, err
}

data.ID = types.StringValue(serviceMetadataID(serviceName, namespace))
data.ServiceName = types.StringValue(serviceName)
data.Namespace = types.StringValue(namespace)

if metadataResp.ServiceMetaData == nil {
if /* expected not to be deleted */ !data.MetadataJSON.IsNull() {
data.MetadataJSON = jsontypes.NewNormalizedValue("")
}
return data, nil
}

metadataJSON, err := json.Marshal(metadataResp.ServiceMetaData)
if err != nil {
return ServiceMetadataModel{}, fmt.Errorf("failed to marshal result: %w", err)
}

data.MetadataJSON = jsontypes.NewNormalizedValue(string(metadataJSON))
return data, nil
}

func (m *ServiceMetadataModel) Validate(base path.Path) (diags diag.Diagnostics) {
if m.ID.IsNull() || m.ID.IsUnknown() {
return
}
id := m.ID.ValueString()
idPath := base.AtName("id")

serviceName, namespace, err := parseServiceMetadataID(id)
if err != nil {
diags.AddAttributeError(
idPath,
"Invalid ID",
err.Error(),
)
return
}

if !m.ServiceName.IsNull() && !m.ServiceName.IsUnknown() && m.ServiceName.ValueString() != serviceName {
diags.AddAttributeError(
idPath,
"Invalid ID",
fmt.Sprintf("ID is expected to start with '%s/', but got: '%s'", m.ServiceName.ValueString(), id),
)
}
if !m.Namespace.IsNull() && !m.Namespace.IsUnknown() && m.Namespace.ValueString() != namespace {
diags.AddAttributeError(
idPath,
"Invalid ID",
fmt.Sprintf("ID is expected to end with '/%s', but got: '%s'", m.Namespace.ValueString(), id),
)
}

return
}

func (m *ServiceMetadataModel) CreateOrUpdateMetadata(_ context.Context, client *Client) error {
serviceName, namespace, err := m.getID()
if err != nil {
return err
}

var metadata mackerel.ServiceMetaData

if err := json.Unmarshal(
[]byte(m.MetadataJSON.ValueString()), &metadata,
); err != nil {
return fmt.Errorf("failed to unmarshal metadata: %w", err)
}

if err := client.PutServiceMetaData(serviceName, namespace, metadata); err != nil {
return err
}

m.ID = types.StringValue(serviceMetadataID(serviceName, namespace))
return nil
}

func (m *ServiceMetadataModel) Delete(_ context.Context, client *Client) error {
serviceName, namespace, err := m.getID()
if err != nil {
return err
}

if err := client.DeleteServiceMetaData(serviceName, namespace); err != nil {
return err
}

return nil
}

func (m *ServiceMetadataModel) getID() (serviceName, namespace string, err error) {
if !m.ID.IsNull() && !m.ID.IsUnknown() {
return parseServiceMetadataID(m.ID.ValueString())
}
return m.ServiceName.ValueString(), m.Namespace.ValueString(), nil
}
171 changes: 171 additions & 0 deletions internal/mackerel/service_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package mackerel

import (
"context"
"fmt"
"slices"
"testing"

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

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

cases := map[string]struct {
inClient serviceMetadataGetterFunc
in ServiceMetadataModel

wants ServiceMetadataModel
wantFail bool
}{
"from service name and namespace": {
inClient: func(s, ns string) (*mackerel.ServiceMetaDataResp, error) {
if s != "service0" || ns != "data0" {
return nil, fmt.Errorf("no metadata found")
}
return &mackerel.ServiceMetaDataResp{
ServiceMetaData: map[string]any{
"foo": "bar",
},
}, nil
},
in: ServiceMetadataModel{
ID: types.StringUnknown(),
ServiceName: types.StringValue("service0"),
Namespace: types.StringValue("data0"),
},

wants: ServiceMetadataModel{
ID: types.StringValue("service0/data0"),
ServiceName: types.StringValue("service0"),
Namespace: types.StringValue("data0"),
MetadataJSON: jsontypes.NewNormalizedValue(`{"foo":"bar"}`),
},
},
"from id": {
inClient: func(s, ns string) (*mackerel.ServiceMetaDataResp, error) {
if s != "service0" || ns != "data0" {
return nil, fmt.Errorf("no metadata found")
}
return &mackerel.ServiceMetaDataResp{
ServiceMetaData: map[string]any{
"foo": "bar",
},
}, nil
},
in: ServiceMetadataModel{
ID: types.StringValue("service0/data0"),
},

wants: ServiceMetadataModel{
ID: types.StringValue("service0/data0"),
ServiceName: types.StringValue("service0"),
Namespace: types.StringValue("data0"),
MetadataJSON: jsontypes.NewNormalizedValue(`{"foo":"bar"}`),
},
},
}

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

actual, err := readServiceMetadataInner(ctx, tt.inClient, tt.in)
if (err != nil) != tt.wantFail {
if tt.wantFail {
t.Errorf("unexpected success")
} else {
t.Errorf("unexpected error: %+v", err)
}
return
}

if diff := cmp.Diff(tt.wants, actual); diff != "" {
t.Errorf("%s", diff)
}
})
}

}

type serviceMetadataGetterFunc func(string, string) (*mackerel.ServiceMetaDataResp, error)

func (f serviceMetadataGetterFunc) GetServiceMetaData(serviceName, namespace string) (*mackerel.ServiceMetaDataResp, error) {
return f(serviceName, namespace)
}

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

cases := map[string]struct {
in ServiceMetadataModel

wantError bool
wantErrorIn path.Expressions
}{
"valid": {
in: ServiceMetadataModel{
ID: types.StringValue("service/namespace"),
ServiceName: types.StringValue("service"),
Namespace: types.StringValue("namespace"),
},
},
"invalid id syntax": {
in: ServiceMetadataModel{
ID: types.StringValue("service,namespace"),
},
wantError: true,
wantErrorIn: path.Expressions{path.MatchRoot("id")},
},
"unmatched service": {
in: ServiceMetadataModel{
ID: types.StringValue("service0/namespace"),
ServiceName: types.StringValue("service1"),
Namespace: types.StringValue("namespace"),
},
wantError: true,
wantErrorIn: path.Expressions{path.MatchRoot("id")},
},
"unmatched namespace": {
in: ServiceMetadataModel{
ID: types.StringValue("service/namespace0"),
ServiceName: types.StringValue("service"),
Namespace: types.StringValue("namespace1"),
},
wantError: true,
wantErrorIn: path.Expressions{path.MatchRoot("id")},
},
}

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

diags := tt.in.Validate(path.Empty())
for _, d := range diags {
if d.Severity() != diag.SeverityError {
continue
}
dwp, ok := d.(diag.DiagnosticWithPath)
if ok {
p := dwp.Path()
if slices.ContainsFunc(tt.wantErrorIn, func(expr path.Expression) bool {
return expr.Matches(p)
}) {
continue
}
} else if tt.wantError {
continue
}
t.Errorf("unexpected error: %v", d)
}
})
}
}
27 changes: 27 additions & 0 deletions internal/provider/data_source_mackerel_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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_MackerelServiceMetadataDataSource_schema(t *testing.T) {
t.Parallel()

ctx := context.Background()

req := fwdatasource.SchemaRequest{}
resp := &fwdatasource.SchemaResponse{}
provider.NewMackerelServiceMetadataDataSource().Schema(ctx, req, resp)
if resp.Diagnostics.HasError() {
t.Errorf("schema method: %+v", resp.Diagnostics)
return
}

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

0 comments on commit 4fcbdf4

Please sign in to comment.