Skip to content

Commit

Permalink
WIP: implement IntOrString as DynamicType
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Hoß <[email protected]>
  • Loading branch information
sebhoss committed Apr 2, 2024
1 parent fdcfb7a commit 963b3fa
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,45 @@ data "k8s_monitoring_coreos_com_pod_monitor_v1_manifest" "example" {
}
}
}

data "k8s_monitoring_coreos_com_pod_monitor_v1_manifest" "int_target_port" {
metadata = {
name = "some-name"
namespace = "some-namespace"
}
spec = {
pod_metrics_endpoints = [
{
path = "/metrics"
port = "metrics"
target_port = 1234
}
]
selector = {
match_labels = {
"key" = "value"
}
}
}
}

data "k8s_monitoring_coreos_com_pod_monitor_v1_manifest" "string_target_port" {
metadata = {
name = "some-name"
namespace = "some-namespace"
}
spec = {
pod_metrics_endpoints = [
{
path = "/metrics"
port = "metrics"
target_port = "http"
}
]
selector = {
match_labels = {
"key" = "value"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
output "manifests" {
value = {
"example" = data.k8s_monitoring_coreos_com_pod_monitor_v1_manifest.example.yaml
"int_target_port" = data.k8s_monitoring_coreos_com_pod_monitor_v1_manifest.int_target_port.yaml
"string_target_port" = data.k8s_monitoring_coreos_com_pod_monitor_v1_manifest.string_target_port.yaml
}
}
164 changes: 164 additions & 0 deletions internal/customtypes/int_or_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* SPDX-FileCopyrightText: The terraform-provider-k8s Authors
* SPDX-License-Identifier: 0BSD
*/

package customtypes

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"math/big"
yaml "sigs.k8s.io/yaml/goyaml.v3"
"strconv"
)

var (
_ basetypes.DynamicTypable = IntOrStringType{}
_ xattr.TypeWithValidate = IntOrStringType{}
_ basetypes.DynamicValuable = IntOrStringValue{}
_ yaml.Marshaler = IntOrStringValue{}
)

type IntOrStringType struct {
basetypes.DynamicType
// ... potentially other fields ...
}

type IntOrStringValue struct {
basetypes.DynamicValue
// ... potentially other fields ...
}

func (t IntOrStringType) Equal(o attr.Type) bool {
other, ok := o.(IntOrStringType)

if !ok {
return false
}

return t.DynamicType.Equal(other.DynamicType)
}

func (t IntOrStringType) String() string {
return "IntOrStringType"
}

func (t IntOrStringType) ValueFromDynamic(ctx context.Context, in basetypes.DynamicValue) (basetypes.DynamicValuable, diag.Diagnostics) {
value := IntOrStringValue{
DynamicValue: in,
}

return value, nil
}

func (t IntOrStringType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.DynamicType.ValueFromTerraform(ctx, in)

if err != nil {
return nil, err
}

dynamicValue, ok := attrValue.(basetypes.DynamicValue)

if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}

dynamicValuable, diags := t.ValueFromDynamic(ctx, dynamicValue)

if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting DynamicValue to DynamicValuable: %v", diags)
}

return dynamicValuable, nil
}

func (t IntOrStringType) ValueType(ctx context.Context) attr.Value {
return IntOrStringValue{}
}

func (t IntOrStringType) Validate(ctx context.Context, value tftypes.Value, path path.Path) diag.Diagnostics {
if value.IsNull() || !value.IsKnown() {
return nil
}

var diags diag.Diagnostics

valueType := value.Type()

if !valueType.Is(tftypes.String) && !valueType.Is(tftypes.Number) {
diags.AddAttributeError(
path,
"Invalid Terraform Value",
"IntOrString type only accepts values of type tftypes.String or tftypes.Number but got "+valueType.String()+". "+
"Make sure your Terraform configuration matches this expectation.\n\n"+
"Path: "+path.String(),
)
return diags
}

return diags
}

func (v IntOrStringValue) Equal(o attr.Value) bool {
other, ok := o.(IntOrStringValue)

if !ok {
return false
}

return v.DynamicValue.Equal(other.DynamicValue)
}

func (v IntOrStringValue) Type(ctx context.Context) attr.Type {
return IntOrStringType{}
}

func (v IntOrStringValue) MarshalYAML() (interface{}, error) {
if v.IsUnknown() || v.IsNull() {
return nil, nil
}

tfValue, err := v.UnderlyingValue().ToTerraformValue(context.Background())
if err != nil {
return nil, err
}

switch {
case tfValue.Type().Is(tftypes.String):
var yamlValue string
err = tfValue.As(&yamlValue)
if err != nil {
return nil, err
}
i, err := strconv.Atoi(yamlValue)
if err == nil {
return i, nil
}
return yamlValue, nil
case tfValue.Type().Is(tftypes.Number):
var yamlValue big.Float
err = tfValue.As(&yamlValue)
if err != nil {
return nil, err
}
if yamlValue.IsInt() {
inv, acc := yamlValue.Int64()
if acc != big.Exact {
return nil, fmt.Errorf("%s inexact integer approximation when converting number value", v.String())
}
return inv, nil
} else {
return nil, fmt.Errorf("%s is not an integer", v.String())
}
default:
return nil, nil
}
}
170 changes: 170 additions & 0 deletions internal/customtypes/int_or_string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* SPDX-FileCopyrightText: The terraform-provider-k8s Authors
* SPDX-License-Identifier: 0BSD
*/

package customtypes

import (
"context"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"math/big"
yaml "sigs.k8s.io/yaml/goyaml.v3"
"testing"
)

func TestIntOrStringTypeTerraformType(t *testing.T) {
result := IntOrStringType{}.TerraformType(context.Background())
if diff := cmp.Diff(result, tftypes.DynamicPseudoType); diff != "" {
t.Errorf("unexpected result (+expected, -got): %s", diff)
}
}

func TestIntOrStringTypeValueFromTerraform(t *testing.T) {
type testCase struct {
receiver IntOrStringType
input tftypes.Value
expected attr.Value
}
tests := map[string]testCase{
"string": {
receiver: IntOrStringType{},
input: tftypes.NewValue(tftypes.String, "hello"),
expected: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewStringValue("hello")),
},
},
"number": {
receiver: IntOrStringType{},
input: tftypes.NewValue(tftypes.Number, 123),
expected: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewNumberValue(big.NewFloat(123))),
},
},
"nil": {
receiver: IntOrStringType{},
input: tftypes.NewValue(nil, nil),
expected: IntOrStringValue{
DynamicValue: basetypes.NewDynamicNull(),
},
},
"unknown": {
receiver: IntOrStringType{},
input: tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue),
expected: IntOrStringValue{
DynamicValue: basetypes.NewDynamicUnknown(),
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, err := test.receiver.ValueFromTerraform(context.Background(), test.input)
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
return
}
if diff := cmp.Diff(test.expected, got); diff != "" {
t.Errorf("unexpected result (-expected, +got): %s", diff)
}
if test.expected != nil && test.expected.IsNull() != test.input.IsNull() {
t.Errorf("Expected null-ness match: expected %t, got %t", test.expected.IsNull(), test.input.IsNull())
}
if test.expected != nil && test.expected.IsUnknown() != !test.input.IsKnown() {
t.Errorf("Expected unknown-ness match: expected %t, got %t", test.expected.IsUnknown(), !test.input.IsKnown())
}
})
}
}

func TestIntOrStringValueToTerraformValue(t *testing.T) {
type testCase struct {
receiver IntOrStringValue
expected tftypes.Value
}
tests := map[string]testCase{
"string": {
receiver: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewStringValue("hello")),
},
expected: tftypes.NewValue(tftypes.String, "hello"),
},
"number": {
receiver: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewNumberValue(big.NewFloat(123))),
},
expected: tftypes.NewValue(tftypes.Number, 123),
},
"unknown": {
receiver: IntOrStringValue{
DynamicValue: basetypes.NewDynamicUnknown(),
},
expected: tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue),
},
"null": {
receiver: IntOrStringValue{
DynamicValue: basetypes.NewDynamicNull(),
},
expected: tftypes.NewValue(tftypes.DynamicPseudoType, nil),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, err := test.receiver.ToTerraformValue(context.Background())
if err != nil {
t.Errorf("Unexpected error: %s", err)
return
}
if diff := cmp.Diff(test.expected, got); diff != "" {
t.Errorf("Unexpected diff (+wanted, -got): %s", diff)
}
})
}
}

func TestIntOrStringValueYamlMarshaller(t *testing.T) {
type testCase struct {
value attr.Value
expected string
}
tests := map[string]testCase{
"int": {
value: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewNumberValue(big.NewFloat(123))),
},
expected: "123",
},
"string": {
value: IntOrStringValue{
DynamicValue: basetypes.NewDynamicValue(basetypes.NewStringValue("hello")),
},
expected: "hello",
},
"null": {
value: IntOrStringValue{
DynamicValue: basetypes.NewDynamicNull(),
},
expected: "null",
},
"unknown": {
value: IntOrStringValue{
DynamicValue: basetypes.NewDynamicUnknown(),
},
expected: "null",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
marshal, err := yaml.Marshal(test.value)
if err != nil {
t.Errorf("Unexpected error: %s", err)
return
}
if diff := cmp.Diff(test.expected+"\n", string(marshal)); diff != "" {
t.Errorf("unexpected result (-expected, +got): %s", diff)
}
})
}
}
Loading

0 comments on commit 963b3fa

Please sign in to comment.