Skip to content

Commit b9a20b5

Browse files
committed
Use OpenAPI v3 endopint for manifest schema discovery
1 parent 6dbc5a0 commit b9a20b5

File tree

9 files changed

+331
-137
lines changed

9 files changed

+331
-137
lines changed

manifest/openapi/foundry_v2.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ func NewFoundryFromSpecV2(spec []byte) (Foundry, error) {
3636

3737
f := foapiv2{
3838
swagger: &swg,
39-
typeCache: sync.Map{},
4039
gkvIndex: sync.Map{}, //reverse lookup index from GVK to OpenAPI definition IDs
4140
recursionDepth: 50, // arbitrarily large number - a type this deep will likely kill Terraform anyway
4241
gate: sync.Mutex{},
@@ -57,7 +56,6 @@ type Foundry interface {
5756

5857
type foapiv2 struct {
5958
swagger *openapi2.T
60-
typeCache sync.Map
6159
gkvIndex sync.Map
6260
recursionDepth uint64 // a last resort circuit-breaker for run-away recursion - hitting this will make for a bad day
6361
gate sync.Mutex
@@ -105,7 +103,7 @@ func (f *foapiv2) getTypeByID(id string, h map[string]string, ap tftypes.Attribu
105103
return nil, fmt.Errorf("failed to resolve schema: %s", err)
106104
}
107105

108-
return getTypeFromSchema(sch, f.recursionDepth, &(f.typeCache), f.swagger.Definitions, ap, h)
106+
return getTypeFromSchema(sch, f.recursionDepth, f.swagger.Definitions, ap, h)
109107
}
110108

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

manifest/openapi/foundry_v3.go

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package openapi
55

66
import (
7+
"encoding/json"
78
"fmt"
89
"sync"
910

@@ -18,49 +19,89 @@ func NewFoundryFromSpecV3(spec []byte) (Foundry, error) {
1819
if err != nil {
1920
return nil, err
2021
}
21-
return &foapiv3{doc: oapi3}, nil
22+
f := &foapiv3{doc: oapi3}
23+
24+
err = f.buildGvkIndex()
25+
if err != nil {
26+
return nil, fmt.Errorf("failed to build GVK index when creating new foundry: %w", err)
27+
}
28+
29+
return f, nil
2230
}
2331

24-
func SchemaToSpec(key string, crschema map[string]interface{}) map[string]interface{} {
25-
schema := make(map[string]interface{})
32+
func CRDSchemaToSpec(gvk schema.GroupVersionKind, crschema map[string]any) map[string]any {
33+
34+
schema := make(map[string]any)
2635
for k, v := range crschema {
2736
schema[k] = v
2837
}
29-
return map[string]interface{}{
38+
schema["x-kubernetes-group-version-kind"] = []map[string]any{
39+
{
40+
"group": gvk.Group,
41+
"version": gvk.Version,
42+
"kind": gvk.Kind,
43+
},
44+
}
45+
return map[string]any{
3046
"openapi": "3.0",
31-
"info": map[string]interface{}{
47+
"info": map[string]any{
3248
"title": "CRD schema wrapper",
3349
"version": "1.0.0",
3450
},
35-
"paths": map[string]interface{}{},
36-
"components": map[string]interface{}{
37-
"schemas": map[string]interface{}{
38-
key: schema,
51+
"paths": map[string]any{},
52+
"components": map[string]any{
53+
"schemas": map[string]any{
54+
"crd-schema": schema,
3955
},
4056
},
4157
}
4258
}
4359

4460
type foapiv3 struct {
4561
doc *openapi3.T
46-
gate sync.Mutex
47-
typeCache sync.Map
62+
gkvIndex sync.Map
4863
}
4964

50-
func (f *foapiv3) GetTypeByGVK(_ schema.GroupVersionKind) (tftypes.Type, map[string]string, error) {
51-
f.gate.Lock()
52-
defer f.gate.Unlock()
53-
65+
func (f *foapiv3) GetTypeByGVK(gvk schema.GroupVersionKind) (tftypes.Type, map[string]string, error) {
5466
var hints map[string]string = make(map[string]string)
55-
ap := tftypes.AttributePath{}
5667

57-
sref := f.doc.Components.Schemas[""]
68+
// the ID string that OpenAPI uses to identify the resource
69+
// e.g. "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
70+
id, ok := f.gkvIndex.Load(gvk)
71+
if !ok {
72+
return nil, nil, fmt.Errorf("resource not found in OpenAPI index")
73+
}
5874

75+
sref := f.doc.Components.Schemas[id.(string)]
5976
sch, err := resolveSchemaRef(sref, f.doc.Components.Schemas)
6077
if err != nil {
61-
return nil, hints, fmt.Errorf("failed to resolve schema: %s", err)
78+
return nil, hints, fmt.Errorf("failed to resolve schema: %w", err)
6279
}
6380

64-
tftype, err := getTypeFromSchema(sch, 50, &(f.typeCache), f.doc.Components.Schemas, ap, hints)
81+
tftype, err := getTypeFromSchema(sch, 50, f.doc.Components.Schemas, tftypes.AttributePath{}, hints)
6582
return tftype, hints, err
6683
}
84+
85+
// buildGvkIndex builds the reverse lookup index that associates each GVK
86+
// to its corresponding string key in the swagger.Definitions map
87+
func (f *foapiv3) buildGvkIndex() error {
88+
for did, dRef := range f.doc.Components.Schemas {
89+
def, err := resolveSchemaRef(dRef, f.doc.Components.Schemas)
90+
if err != nil {
91+
return err
92+
}
93+
ex, ok := def.Extensions["x-kubernetes-group-version-kind"]
94+
if !ok {
95+
continue
96+
}
97+
gvk := []schema.GroupVersionKind{}
98+
err = json.Unmarshal(([]byte)(ex.(json.RawMessage)), &gvk)
99+
if err != nil {
100+
return fmt.Errorf("failed to unmarshall GVK from OpenAPI schema extention: %w", err)
101+
}
102+
for i := range gvk {
103+
f.gkvIndex.Store(gvk[i], did)
104+
}
105+
}
106+
return nil
107+
}

manifest/openapi/foundry_v3_test.go

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package openapi
66
import (
77
"encoding/json"
88
"testing"
9+
10+
"k8s.io/apimachinery/pkg/runtime/schema"
911
)
1012

1113
func TestNewFoundryFromSpecV3(t *testing.T) {
@@ -20,49 +22,58 @@ func TestNewFoundryFromSpecV3(t *testing.T) {
2022
},
2123
"type": "object",
2224
}
23-
24-
spec := SchemaToSpec("com.hashicorp.v1.TestCrd", sampleSchema)
25+
gvk := schema.FromAPIVersionAndKind("hashicorp.com/v1", "TestCrd")
26+
spec := CRDSchemaToSpec(gvk, sampleSchema)
2527
j, err := json.Marshal(spec)
2628
if err != nil {
2729
t.Fatalf("Error: %+v", err)
2830
}
2931

3032
f, err := NewFoundryFromSpecV3(j)
3133
if err != nil {
32-
t.Fatalf("Error: %+v", err)
34+
t.Fatalf("Error creating foundry: %v", err)
35+
}
36+
37+
f3, ok := f.(*foapiv3)
38+
if !ok {
39+
t.Fatal("foundry not of expected type")
3340
}
3441

35-
if f.(*foapiv3).doc == nil {
36-
t.Fail()
42+
if f3.doc == nil {
43+
t.Fatal("no doc")
3744
}
38-
if f.(*foapiv3).doc.Components.Schemas == nil {
39-
t.Fail()
45+
if f3.doc.Components.Schemas == nil {
46+
t.Fatal("no schemas")
47+
}
48+
id, ok := f3.gkvIndex.Load(gvk)
49+
if !ok {
50+
t.Fatal("could not lookup schema id")
4051
}
41-
crd, ok := f.(*foapiv3).doc.Components.Schemas["com.hashicorp.v1.TestCrd"]
52+
crd, ok := f3.doc.Components.Schemas[id.(string)]
4253
if !ok {
43-
t.Fail()
54+
t.Fatal("CRD schema not found")
4455
}
4556
if crd == nil || crd.Value == nil {
46-
t.Fail()
57+
t.Fatal("CRD schema empty")
4758
}
4859
if crd.Value.Type != "object" {
49-
t.Fail()
60+
t.Fatal("CRD type not object")
5061
}
5162
if crd.Value.Properties == nil {
52-
t.Fail()
63+
t.Fatal("CRD missing properties")
5364
}
5465
foo, ok := crd.Value.Properties["foo"]
5566
if !ok {
56-
t.Fail()
67+
t.Fatal("CRD missing property foo")
5768
}
5869
if foo.Value.Type != "string" {
59-
t.Fail()
70+
t.Fatal("CRD property foo not a string")
6071
}
6172
bar, ok := crd.Value.Properties["bar"]
6273
if !ok {
63-
t.Fail()
74+
t.Fatal("CRD missing property bar")
6475
}
6576
if bar.Value.Type != "number" {
66-
t.Fail()
77+
t.Fatal("CRD property bar not a number")
6778
}
6879
}

0 commit comments

Comments
 (0)