Skip to content

Commit 1340e20

Browse files
authored
Implement type hinting - fixes IntOrString handling (#1590)
Implement type hinting mechanism. We use this to carry over additional metadata from the OpenAPI conversion, in case the resulting types are aproximated with tftypes. The current use-case is the aproximation of IntOrString as tftypes.String while still being able to reconstruct it accurately when generating the API request payload. Also: * Extend Service manifest example with two types of targetPort values * Choose log level from all available env variables or default to off.
1 parent 5cfbb54 commit 1340e20

29 files changed

+771
-200
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ website/vendor
3131
# Test exclusions
3232
!command/test-fixtures/**/*.tfstate
3333
!command/test-fixtures/**/.terraform/
34+
35+
# output binary
36+
terraform-provider-kubernetes

_examples/kubernetes_manifest/service/service.tf

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,15 @@ resource "kubernetes_manifest" "service-injector" {
2323
"spec" = {
2424
"ports" = [
2525
{
26+
"name" = "http"
27+
"port" = 80
28+
"targetPort" = 8080
29+
"protocol" = "TCP"
30+
},
31+
{
32+
"name" = "https"
2633
"port" = 443
27-
"targetPort" = 80
34+
"targetPort" = "https"
2835
"protocol" = "TCP"
2936
},
3037
]

manifest/openapi/foundry_v2.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func NewFoundryFromSpecV2(spec []byte) (Foundry, error) {
4949

5050
// Foundry is a mechanism to construct tftypes out of OpenAPI specifications
5151
type Foundry interface {
52-
GetTypeByGVK(gvk schema.GroupVersionKind) (tftypes.Type, error)
52+
GetTypeByGVK(gvk schema.GroupVersionKind) (tftypes.Type, map[string]string, error)
5353
}
5454

5555
type foapiv2 struct {
@@ -62,26 +62,31 @@ type foapiv2 struct {
6262

6363
// GetTypeByGVK looks up a type by its GVK in the Definitions sections of
6464
// the OpenAPI spec and returns its (nearest) tftypes.Type equivalent
65-
func (f *foapiv2) GetTypeByGVK(gvk schema.GroupVersionKind) (tftypes.Type, error) {
65+
func (f *foapiv2) GetTypeByGVK(gvk schema.GroupVersionKind) (tftypes.Type, map[string]string, error) {
6666
f.gate.Lock()
6767
defer f.gate.Unlock()
6868

69+
var hints map[string]string = make(map[string]string)
70+
ap := tftypes.AttributePath{}
71+
6972
// ObjectMeta isn't discoverable via the index because it's not tagged with "x-kubernetes-group-version-kind" in OpenAPI spec
7073
// as top-level resouces schemas are. But we need ObjectMeta as a separate type when backfilling into CRD schemas.
7174
if gvk == ObjectMetaGVK {
72-
return f.getTypeByID("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta")
75+
t, err := f.getTypeByID("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", hints, ap)
76+
return t, hints, err
7377
}
7478

7579
// the ID string that Swagger / OpenAPI uses to identify the resource
7680
// e.g. "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
7781
id, ok := f.gkvIndex.Load(gvk)
7882
if !ok {
79-
return nil, fmt.Errorf("%v resource not found in OpenAPI index", gvk)
83+
return nil, nil, fmt.Errorf("%v resource not found in OpenAPI index", gvk)
8084
}
81-
return f.getTypeByID(id.(string))
85+
t, err := f.getTypeByID(id.(string), hints, ap)
86+
return t, hints, err
8287
}
8388

84-
func (f *foapiv2) getTypeByID(id string) (tftypes.Type, error) {
89+
func (f *foapiv2) getTypeByID(id string, h map[string]string, ap tftypes.AttributePath) (tftypes.Type, error) {
8590
swd, ok := f.swagger.Definitions[id]
8691

8792
if !ok {
@@ -97,7 +102,7 @@ func (f *foapiv2) getTypeByID(id string) (tftypes.Type, error) {
97102
return nil, fmt.Errorf("failed to resolve schema: %s", err)
98103
}
99104

100-
return getTypeFromSchema(sch, f.recursionDepth, &(f.typeCache), f.swagger.Definitions)
105+
return getTypeFromSchema(sch, f.recursionDepth, &(f.typeCache), f.swagger.Definitions, ap, h)
101106
}
102107

103108
// buildGvkIndex builds the reverse lookup index that associates each GVK

manifest/openapi/foundry_v2_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
type testSample struct {
15-
gvk schema.GroupVersionKind
16-
want tftypes.Type
15+
gvk schema.GroupVersionKind
16+
hints map[string]string
17+
want tftypes.Type
1718
}
1819

1920
type testSamples map[string]testSample
@@ -29,8 +30,8 @@ var objectMetaType = tftypes.Object{
2930
"generateName": tftypes.String,
3031
"generation": tftypes.Number,
3132
"labels": tftypes.Map{ElementType: tftypes.String},
32-
"managedFields": tftypes.List{
33-
ElementType: tftypes.Object{
33+
"managedFields": tftypes.Tuple{
34+
ElementTypes: []tftypes.Type{tftypes.Object{
3435
AttributeTypes: map[string]tftypes.Type{
3536
"apiVersion": tftypes.String,
3637
"fieldsType": tftypes.String,
@@ -39,7 +40,7 @@ var objectMetaType = tftypes.Object{
3940
"operation": tftypes.String,
4041
"time": tftypes.String,
4142
},
42-
},
43+
}},
4344
},
4445
"name": tftypes.String,
4546
"namespace": tftypes.String,
@@ -63,7 +64,8 @@ var objectMetaType = tftypes.Object{
6364

6465
var samples = testSamples{
6566
"core.v1/ConfigMap": {
66-
gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
67+
gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
68+
hints: map[string]string{},
6769
want: tftypes.Object{
6870
AttributeTypes: map[string]tftypes.Type{
6971
"apiVersion": tftypes.String,
@@ -77,6 +79,9 @@ var samples = testSamples{
7779
},
7880
"core.v1/Service": {
7981
gvk: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"},
82+
hints: map[string]string{
83+
"AttributeName(\"spec\").AttributeName(\"ports\").ElementKeyInt(-1).AttributeName(\"targetPort\")": "io.k8s.apimachinery.pkg.util.intstr.IntOrString",
84+
},
8085
want: tftypes.Object{
8186
AttributeTypes: map[string]tftypes.Type{
8287
"apiVersion": tftypes.String,
@@ -92,17 +97,17 @@ var samples = testSamples{
9297
"ipFamily": tftypes.String,
9398
"loadBalancerIP": tftypes.String,
9499
"loadBalancerSourceRanges": tftypes.List{ElementType: tftypes.String},
95-
"ports": tftypes.List{
96-
ElementType: tftypes.Object{
100+
"ports": tftypes.Tuple{
101+
ElementTypes: []tftypes.Type{tftypes.Object{
97102
AttributeTypes: map[string]tftypes.Type{
98103
"appProtocol": tftypes.String,
99104
"name": tftypes.String,
100105
"nodePort": tftypes.Number,
101106
"port": tftypes.Number,
102107
"protocol": tftypes.String,
103-
"targetPort": tftypes.DynamicPseudoType,
108+
"targetPort": tftypes.String,
104109
},
105-
},
110+
}},
106111
},
107112
"publishNotReadyAddresses": tftypes.Bool,
108113
"selector": tftypes.Map{ElementType: tftypes.String},
@@ -141,12 +146,15 @@ func TestGetType(t *testing.T) {
141146
for name, s := range samples {
142147
t.Run(name,
143148
func(t *testing.T) {
144-
rt, err := tf.GetTypeByGVK(s.gvk)
149+
rt, th, err := tf.GetTypeByGVK(s.gvk)
145150
if err != nil {
146151
t.Fatal(fmt.Errorf("GetTypeByID() failed: %s", err))
147152
}
148153
if !rt.Is(s.want) {
149-
t.Fatalf("\nRETURNED %#v\nEXPECTED: %#v", rt, s.want)
154+
t.Fatalf("\nRETURNED type: %#v\nEXPECTED type: %#v", rt, s.want)
155+
}
156+
if len(th) != len(s.hints) {
157+
t.Fatalf("\nRETURNED hints: %#v\nEXPECTED hints: %#v", th, s.hints)
150158
}
151159
})
152160
}

manifest/openapi/foundry_v3.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,20 @@ type foapiv3 struct {
4444
typeCache sync.Map
4545
}
4646

47-
func (f *foapiv3) GetTypeByGVK(_ schema.GroupVersionKind) (tftypes.Type, error) {
47+
func (f *foapiv3) GetTypeByGVK(_ schema.GroupVersionKind) (tftypes.Type, map[string]string, error) {
4848
f.gate.Lock()
4949
defer f.gate.Unlock()
5050

51+
var hints map[string]string = make(map[string]string)
52+
ap := tftypes.AttributePath{}
53+
5154
sref := f.doc.Components.Schemas[""]
5255

5356
sch, err := resolveSchemaRef(sref, f.doc.Components.Schemas)
5457
if err != nil {
55-
return nil, fmt.Errorf("failed to resolve schema: %s", err)
58+
return nil, hints, fmt.Errorf("failed to resolve schema: %s", err)
5659
}
5760

58-
tftype, err := getTypeFromSchema(sch, 50, &(f.typeCache), f.doc.Components.Schemas)
59-
return tftype, err
61+
tftype, err := getTypeFromSchema(sch, 50, &(f.typeCache), f.doc.Components.Schemas, ap, hints)
62+
return tftype, hints, err
6063
}

manifest/openapi/schema.go

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package openapi
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"strings"
@@ -32,7 +33,8 @@ func resolveSchemaRef(ref *openapi3.SchemaRef, defs map[string]*openapi3.SchemaR
3233
switch sid {
3334
case "io.k8s.apimachinery.pkg.util.intstr.IntOrString":
3435
t := openapi3.Schema{
35-
Type: "",
36+
Type: "string",
37+
Description: "io.k8s.apimachinery.pkg.util.intstr.IntOrString", // this value later carries over as the "type hint"
3638
}
3739
return &t, nil
3840
case "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps":
@@ -45,23 +47,12 @@ func resolveSchemaRef(ref *openapi3.SchemaRef, defs map[string]*openapi3.SchemaR
4547
Type: "",
4648
}
4749
return &t, nil
48-
case "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinitionSpec":
49-
t, err := resolveSchemaRef(nref, defs)
50-
if err != nil {
51-
return nil, fmt.Errorf("failed to resolve schema: %s", err)
52-
}
53-
vs := t.Properties["versions"]
54-
if vs.Value.AdditionalProperties == nil && vs.Value.Items != nil {
55-
vs.Value.AdditionalProperties = vs.Value.Items
56-
vs.Value.Items = nil
57-
}
58-
return t, nil
5950
}
6051

6152
return resolveSchemaRef(nref, defs)
6253
}
6354

64-
func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync.Map, defs map[string]*openapi3.SchemaRef) (tftypes.Type, error) {
55+
func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync.Map, defs map[string]*openapi3.SchemaRef, ap tftypes.AttributePath, th map[string]string) (tftypes.Type, error) {
6556
if stackdepth == 0 {
6657
// this is a hack to overcome the inability to express recursion in tftypes
6758
return nil, errors.New("recursion runaway while generating type from OpenAPI spec")
@@ -76,14 +67,18 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
7667
var t tftypes.Type
7768

7869
// check if type is in cache
79-
// HACK: this is temporarily disable to diagnose a cache corruption issue.
70+
// HACK: this is temporarily disabled to diagnose a cache corruption issue.
8071
// if herr == nil {
8172
// if t, ok := typeCache.Load(h); ok {
8273
// return t.(tftypes.Type), nil
8374
// }
8475
// }
8576
switch elem.Type {
8677
case "string":
78+
switch elem.Description {
79+
case "io.k8s.apimachinery.pkg.util.intstr.IntOrString":
80+
th[ap.String()] = "io.k8s.apimachinery.pkg.util.intstr.IntOrString"
81+
}
8782
return tftypes.String, nil
8883

8984
case "boolean":
@@ -96,6 +91,18 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
9691
return tftypes.Number, nil
9792

9893
case "":
94+
if xv, ok := elem.Extensions["x-kubernetes-int-or-string"]; ok {
95+
xb, err := xv.(json.RawMessage).MarshalJSON()
96+
if err != nil {
97+
return tftypes.DynamicPseudoType, nil
98+
}
99+
var x bool
100+
err = json.Unmarshal(xb, &x)
101+
if err == nil && x {
102+
th[ap.String()] = "io.k8s.apimachinery.pkg.util.intstr.IntOrString"
103+
return tftypes.String, nil
104+
}
105+
}
99106
return tftypes.DynamicPseudoType, nil
100107

101108
case "array":
@@ -105,11 +112,16 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
105112
if err != nil {
106113
return nil, fmt.Errorf("failed to resolve schema for items: %s", err)
107114
}
108-
et, err := getTypeFromSchema(it, stackdepth-1, typeCache, defs)
115+
aap := ap.WithElementKeyInt(-1)
116+
et, err := getTypeFromSchema(it, stackdepth-1, typeCache, defs, *aap, th)
109117
if err != nil {
110118
return nil, err
111119
}
112-
t = tftypes.List{ElementType: et}
120+
if !isTypeFullyKnown(et) {
121+
t = tftypes.Tuple{ElementTypes: []tftypes.Type{et}}
122+
} else {
123+
t = tftypes.List{ElementType: et}
124+
}
113125
if herr == nil {
114126
typeCache.Store(h, t)
115127
}
@@ -119,7 +131,8 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
119131
if err != nil {
120132
return nil, fmt.Errorf("failed to resolve schema for items: %s", err)
121133
}
122-
et, err := getTypeFromSchema(it, stackdepth-1, typeCache, defs)
134+
aap := ap.WithElementKeyInt(-1)
135+
et, err := getTypeFromSchema(it, stackdepth-1, typeCache, defs, *aap, th)
123136
if err != nil {
124137
return nil, err
125138
}
@@ -138,7 +151,8 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
138151
if err != nil {
139152
return nil, fmt.Errorf("failed to resolve schema: %s", err)
140153
}
141-
pType, err := getTypeFromSchema(schema, stackdepth-1, typeCache, defs)
154+
aap := ap.WithAttributeName(p)
155+
pType, err := getTypeFromSchema(schema, stackdepth-1, typeCache, defs, *aap, th)
142156
if err != nil {
143157
return nil, err
144158
}
@@ -156,7 +170,8 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
156170
if err != nil {
157171
return nil, fmt.Errorf("failed to resolve schema: %s", err)
158172
}
159-
pt, err := getTypeFromSchema(s, stackdepth-1, typeCache, defs)
173+
aap := ap.WithElementKeyString("#")
174+
pt, err := getTypeFromSchema(s, stackdepth-1, typeCache, defs, *aap, th)
160175
if err != nil {
161176
return nil, err
162177
}
@@ -179,3 +194,30 @@ func getTypeFromSchema(elem *openapi3.Schema, stackdepth uint64, typeCache *sync
179194

180195
return nil, fmt.Errorf("unknown type: %s", elem.Type)
181196
}
197+
198+
func isTypeFullyKnown(t tftypes.Type) bool {
199+
if t.Is(tftypes.DynamicPseudoType) {
200+
return false
201+
}
202+
switch {
203+
case t.Is(tftypes.Object{}):
204+
for _, att := range t.(tftypes.Object).AttributeTypes {
205+
if !isTypeFullyKnown(att) {
206+
return false
207+
}
208+
}
209+
case t.Is(tftypes.Tuple{}):
210+
for _, ett := range t.(tftypes.Tuple).ElementTypes {
211+
if !isTypeFullyKnown(ett) {
212+
return false
213+
}
214+
}
215+
case t.Is(tftypes.List{}):
216+
return isTypeFullyKnown(t.(tftypes.List).ElementType)
217+
case t.Is(tftypes.Set{}):
218+
return isTypeFullyKnown(t.(tftypes.Set).ElementType)
219+
case t.Is(tftypes.Map{}):
220+
return isTypeFullyKnown(t.(tftypes.Map).ElementType)
221+
}
222+
return true
223+
}

0 commit comments

Comments
 (0)