Skip to content

Commit 930cfda

Browse files
rquitalesjustinvp
andauthored
Remove underscores in generated nested types (#114)
* Make all type names PascalCase * Refactor tests * Add tests for camelCased type generation * Add changelog entry --------- Co-authored-by: Justin Van Patten <jvp@justinvp.com>
1 parent 93f89bc commit 930cfda

File tree

5 files changed

+191
-12
lines changed

5 files changed

+191
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
CHANGELOG
22
=========
33

4+
## Unreleased
5+
- Remove underscores in generated nested types (https://github.com/pulumi/crd2pulumi/pull/114)
6+
47
## 1.2.4 (2023-03-23)
58
- Requires Go 1.19 or higher now to build
69
- Fix issue [#108](https://github.com/pulumi/crd2pulumi/issues/108) - crd2pulumi generator splits types apart into duplicate entires in pulumiTypes.go and pulumiTypes1.go

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/pulumi/crd2pulumi
33
go 1.19
44

55
require (
6+
github.com/iancoleman/strcase v0.2.0
67
github.com/pulumi/pulumi/pkg/v3 v3.59.1-0.20230323225522-946074865b11
78
github.com/pulumi/pulumi/sdk/v3 v3.59.1-0.20230323225522-946074865b11
89
github.com/spf13/cobra v1.6.1
@@ -119,7 +120,6 @@ require (
119120
github.com/hashicorp/vault/api v1.8.2 // indirect
120121
github.com/hashicorp/vault/sdk v0.6.1 // indirect
121122
github.com/hashicorp/yamux v0.1.1 // indirect
122-
github.com/iancoleman/strcase v0.2.0 // indirect
123123
github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd // indirect
124124
github.com/imdario/mergo v0.3.13 // indirect
125125
github.com/inconshreveable/mousetrap v1.0.1 // indirect

pkg/codegen/schema.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import (
1919
"sort"
2020
"strconv"
2121

22+
"github.com/iancoleman/strcase"
2223
"github.com/pulumi/crd2pulumi/internal/slices"
2324
"github.com/pulumi/crd2pulumi/internal/unstruct"
2425
pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema"
2526
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
26-
"golang.org/x/text/cases"
27-
"golang.org/x/text/language"
2827
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2928
)
3029

@@ -141,7 +140,7 @@ func AddType(schema map[string]any, name string, types map[string]pschema.Comple
141140
propertyDescription, _, _ := unstructured.NestedString(propertySchema, "description")
142141
defaultValue, _, _ := unstructured.NestedFieldNoCopy(propertySchema, "default")
143142
propertySpecs[propertyName] = pschema.PropertySpec{
144-
TypeSpec: GetTypeSpec(propertySchema, name+cases.Title(language.Und).String(propertyName), types),
143+
TypeSpec: GetTypeSpec(propertySchema, name+strcase.ToCamel(propertyName), types),
145144
Description: propertyDescription,
146145
Default: defaultValue,
147146
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Ensure generated nested types are not underscored.
2+
# See https://github.com/pulumi/crd2pulumi/issues/107
3+
apiVersion: apiextensions.k8s.io/v1
4+
kind: CustomResourceDefinition
5+
metadata:
6+
annotations:
7+
myinfo: abcdefghijkl
8+
generation: 4
9+
labels:
10+
creator.ac.com: myinfo
11+
name: networkpolicies.juice.box.com
12+
spec:
13+
conversion:
14+
strategy: None
15+
group: juice.box.com
16+
names:
17+
kind: NetworkPolicy
18+
listKind: NetworkPolicyList
19+
plural: networkpolicies
20+
shortNames:
21+
- anp
22+
- anps
23+
singular: networkpolicy
24+
preserveUnknownFields: true
25+
scope: Namespaced
26+
versions:
27+
- name: v1alpha1
28+
schema:
29+
openAPIV3Schema:
30+
properties:
31+
spec:
32+
type: object
33+
description: NetworkPolicySpec is a specification of the network
34+
entitlements for a pod.
35+
properties:
36+
apps_incoming:
37+
description: apps_incoming specifies which applications are permitted
38+
to establish a TCP connection to a POD.
39+
items:
40+
properties:
41+
app:
42+
pattern: ^(__kubernetes__|plb\.juice-plb\.juice-prod|((([A-Za-z0-9]+[-A-Za-z0-9]?)*[A-Za-z0-9])\.){2}(kk|kube))$
43+
type: string
44+
cluster:
45+
description: cluster that this policy applies to. Defaults to
46+
the local cluster. Setting cluster to 'ALL' will match all
47+
clusters
48+
type: string
49+
required:
50+
- app
51+
type: object
52+
type: array
53+
apps_outgoing:
54+
description: apps_outgoing specifies what applications a pod may attempt
55+
to make TCP connections to.
56+
items:
57+
properties:
58+
app:
59+
pattern: ^(__kubernetes__|plb\.juice-plb\.juice-prod|((([A-Za-z0-9]+[-A-Za-z0-9]?)*[A-Za-z0-9])\.){2}(kk|kube))$
60+
type: string
61+
cluster:
62+
description: cluster that this policy applies to. Defaults to
63+
the local cluster. Setting cluster to 'ALL' will match all
64+
clusters
65+
type: string
66+
required:
67+
- app
68+
type: object
69+
type: array
70+
namespaces_incoming:
71+
description: namespaces_incoming specifies which kubernetes namespace
72+
are permitted to establish incoming TCP sessions.
73+
items:
74+
properties:
75+
cluster:
76+
description: cluster that this policy applies to. Defaults to
77+
the local cluster. Setting cluster to 'ALL' will match all
78+
clusters
79+
type: string
80+
namespace:
81+
pattern: ^(((([A-Za-z0-9]+[-A-Za-z0-9]?)*[A-Za-z0-9])\.)(kk|kube))$
82+
type: string
83+
required:
84+
- namespace
85+
type: object
86+
type: array
87+
namespaces_outgoing:
88+
description: namespaces_outgoing specifies which kubernetes namespace
89+
are permitted to establish outgoing TCP sessions.
90+
items:
91+
properties:
92+
cluster:
93+
description: cluster that this policy applies to. Defaults to
94+
the local cluster. Setting cluster to 'ALL' will match all
95+
clusters
96+
type: string
97+
namespace:
98+
pattern: ^(((([A-Za-z0-9]+[-A-Za-z0-9]?)*[A-Za-z0-9])\.)(kk|kube))$
99+
type: string
100+
required:
101+
- namespace
102+
type: object
103+
type: array
104+
selector:
105+
additionalProperties:
106+
type: string
107+
description: selector is a set of label selectors
108+
type: object
109+
required:
110+
- selector
111+
served: true
112+
storage: true
113+
status:
114+
acceptedNames:
115+
kind: NetworkPolicy
116+
listKind: NetworkPolicyList
117+
plural: networkpolicies
118+
shortNames:
119+
- anp
120+
- anps
121+
singular: networkpolicy
122+
storedVersions:
123+
- v1alpha1

tests/crds_test.go

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package tests
1616

1717
import (
18+
"fmt"
1819
"io/fs"
1920
"io/ioutil"
2021
"os"
@@ -30,28 +31,44 @@ var languages = []string{"dotnet", "go", "nodejs", "python"}
3031
const gkeManagedCertsUrl = "https://raw.githubusercontent.com/GoogleCloudPlatform/gke-managed-certs/master/deploy/managedcertificates-crd.yaml"
3132

3233
// execCrd2Pulumi runs the crd2pulumi binary in a temporary directory
33-
func execCrd2Pulumi(t *testing.T, lang, path string) {
34-
tmpdir, err := ioutil.TempDir("", "")
34+
func execCrd2Pulumi(t *testing.T, lang, path string, additionalValidation func(t *testing.T, path string)) {
35+
tmpdir, err := ioutil.TempDir("", "crd2pulumi_test")
3536
assert.Nil(t, err, "expected to create a temp dir for the CRD output")
36-
defer os.RemoveAll(tmpdir)
37-
langFlag := "--" + lang + "Path"
37+
t.Cleanup(func() {
38+
t.Logf("removing temp dir %q for %s test", tmpdir, lang)
39+
os.RemoveAll(tmpdir)
40+
})
41+
langFlag := fmt.Sprintf("--%sPath", lang) // e.g. --dotnetPath
3842
binaryPath, err := filepath.Abs("../bin/crd2pulumi")
3943
if err != nil {
40-
panic(err)
44+
t.Fatalf("unable to create absolute path to binary: %s", err)
4145
}
46+
4247
t.Logf("%s %s=%s %s: running", binaryPath, langFlag, tmpdir, path)
4348
crdCmd := exec.Command(binaryPath, langFlag, tmpdir, "--force", path)
4449
crdOut, err := crdCmd.CombinedOutput()
4550
t.Logf("%s %s=%s %s: output=\n%s", binaryPath, langFlag, tmpdir, path, crdOut)
46-
assert.Nil(t, err, "expected crd2pulumi for '%s=%s %s' to succeed", langFlag, tmpdir, path)
51+
if err != nil {
52+
t.Fatalf("expected crd2pulumi for '%s=%s %s' to succeed", langFlag, tmpdir, path)
53+
}
54+
55+
// Run additional validation if provided.
56+
if additionalValidation != nil {
57+
additionalValidation(t, tmpdir)
58+
}
4759
}
4860

4961
// TestCRDsFromFile enumerates all CRD YAML files, and generates them in each language.
5062
func TestCRDsFromFile(t *testing.T) {
5163
filepath.WalkDir("crds", func(path string, d fs.DirEntry, err error) error {
5264
if !d.IsDir() && (filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml") {
5365
for _, lang := range languages {
54-
execCrd2Pulumi(t, lang, path)
66+
lang := lang
67+
name := fmt.Sprintf("%s-%s", lang, filepath.Base(path))
68+
t.Run(name, func(t *testing.T) {
69+
t.Parallel()
70+
execCrd2Pulumi(t, lang, path, nil)
71+
})
5572
}
5673
}
5774
return nil
@@ -61,6 +78,43 @@ func TestCRDsFromFile(t *testing.T) {
6178
// TestCRDsFromUrl pulls the CRD YAML file from a URL and generates it in each language
6279
func TestCRDsFromUrl(t *testing.T) {
6380
for _, lang := range languages {
64-
execCrd2Pulumi(t, lang, gkeManagedCertsUrl)
81+
lang := lang
82+
t.Run(lang, func(t *testing.T) {
83+
t.Parallel()
84+
execCrd2Pulumi(t, lang, gkeManagedCertsUrl, nil)
85+
})
6586
}
6687
}
88+
89+
// TestCRDsWithUnderscore tests that CRDs with underscores field names are camelCased for the
90+
// generated types. Currently this test only runs for Python, and we're hardcoding the field name
91+
// detection logic in the test for simplicity. This is brittle and we should improve this in the
92+
// future.
93+
// TODO: properly detect field names in the generated Python code instead of grep'ing for them.
94+
func TestCRDsWithUnderscore(t *testing.T) {
95+
// Callback function to run additional validation on the generated Python code after running
96+
// crd2pulumi.
97+
validateUnderscore := func(t *testing.T, path string) {
98+
// Ensure inputs are camelCased.
99+
filename := filepath.Join(path, "pulumi_crds", "juice", "v1alpha1", "_inputs.py")
100+
t.Logf("validating underscored field names in %s", filename)
101+
pythonInputs, err := ioutil.ReadFile(filename)
102+
if err != nil {
103+
t.Fatalf("expected to read generated Python code: %s", err)
104+
}
105+
assert.Contains(t, string(pythonInputs), "NetworkPolicySpecAppsIncomingArgs", "expected to find camelCased field name in generated Python code")
106+
assert.NotContains(t, string(pythonInputs), "NetworkPolicySpecApps_incomingArgs", "expected to not find underscored field name in generated Python code")
107+
108+
// Ensure outputs are camelCased.
109+
filename = filepath.Join(path, "pulumi_crds", "juice", "v1alpha1", "outputs.py")
110+
t.Logf("validating underscored field names in %s", filename)
111+
pythonInputs, err = ioutil.ReadFile(filename)
112+
if err != nil {
113+
t.Fatalf("expected to read generated Python code: %s", err)
114+
}
115+
assert.Contains(t, string(pythonInputs), "NetworkPolicySpecAppsIncoming", "expected to find camelCased field name in generated Python code")
116+
assert.NotContains(t, string(pythonInputs), "NetworkPolicySpecApps_incoming", "expected to not find underscored field name in generated Python code")
117+
}
118+
119+
execCrd2Pulumi(t, "python", "crds/underscored-types/networkpolicy.yaml", validateUnderscore)
120+
}

0 commit comments

Comments
 (0)