├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── schema │ └── schema.go └── template-compile │ └── main.go ├── go.mod ├── go.sum ├── hack └── template │ ├── pkg │ ├── generator │ │ ├── compare.go.tmpl │ │ ├── configure.go.tmpl │ │ ├── decode.go.tmpl │ │ ├── doc.go.tmpl │ │ ├── encode.go.tmpl │ │ ├── index.go.tmpl │ │ └── types.go.tmpl │ └── template │ │ └── test-template-getter.txt │ └── provider │ ├── cmd │ └── provider │ │ └── main.go.tpl │ └── generated │ ├── index.go.tpl │ ├── index_provider.go.tpl │ ├── index_resources.go.tpl │ └── provider │ └── v1alpha1 │ ├── doc.go.tpl │ ├── index.go.tpl │ └── types.go.tpl ├── internal └── template │ ├── compiled │ ├── dispatch.go │ ├── pkg │ │ ├── generator │ │ │ ├── compare.go │ │ │ ├── configure.go │ │ │ ├── decode.go │ │ │ ├── doc.go │ │ │ ├── encode.go │ │ │ ├── index.go │ │ │ └── types.go │ │ └── template │ │ │ └── test_template_getter.go │ └── provider │ │ ├── cmd │ │ └── provider │ │ │ └── main.go │ │ └── generated │ │ ├── index.go │ │ ├── index_provider.go │ │ ├── index_resources.go │ │ └── provider │ │ └── v1alpha1 │ │ ├── doc.go │ │ ├── index.go │ │ └── types.go │ ├── generator.go │ └── render.go ├── main.go ├── pkg ├── generator │ ├── errors.go │ ├── errors_test.go │ ├── fragment.go │ ├── interfaces.go │ ├── namer.go │ ├── render.go │ ├── render_test.go │ ├── sort.go │ ├── types.go │ ├── types_stringers.go │ └── types_test.go ├── integration │ ├── fixtures.go │ ├── optimizer_test.go │ ├── render_test.go │ ├── schema_test.go │ ├── testdata │ │ ├── test-provider-binary-schema-s3.go │ │ ├── test-render-duplicate-field-spec.go │ │ ├── test-render-nested-spec.go │ │ ├── test-render-nested-status.go │ │ ├── test-render-types-file.go │ │ └── test-schema-to-managed-resource-render.go │ ├── testing.go │ ├── visitor.go │ └── visitor_test.go ├── optimize │ ├── deduplicator.go │ └── optimizer.go ├── provider │ ├── bootstrap.go │ ├── config.go │ ├── namer.go │ ├── namer_test.go │ ├── package.go │ └── schema.go ├── template │ ├── compiled.go │ ├── fs.go │ ├── fs_test.go │ └── get.go └── translate │ ├── decoders.go │ ├── encoders.go │ ├── encoders_test.go │ ├── mergers.go │ ├── translator_test.go │ └── translators.go └── provider-configs ├── aws.yaml └── vsphere.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | .vscode 3 | *.tf 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 The Crossplane Authors. All rights reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is deprecated. See https://github.com/crossplane-contrib/terrajet. 2 | -------------------------------------------------------------------------------- /cmd/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 8 | ) 9 | 10 | // Dump prints out the schema returned by the provider named by the provider arg 11 | // if the json flag is true, formats the output to json 12 | func Dump(provider *client.Provider, jsonOut bool) error { 13 | schema, err := getProviderSchema(provider) 14 | if err != nil { 15 | return err 16 | } 17 | if jsonOut { 18 | jsonb, err := json.Marshal(schema) 19 | if err != nil { 20 | return err 21 | } 22 | fmt.Println(string(jsonb)) 23 | return nil 24 | } 25 | return nil 26 | } 27 | 28 | type ProviderSchema struct { 29 | Name string 30 | Attributes map[string]schemaAttribute 31 | } 32 | 33 | type schemaAttribute struct { 34 | Type string `json:"type"` 35 | Required bool `json:"required"` 36 | Optional bool `json:"optional"` 37 | Computed bool `json:"computed"` 38 | Description string `json:"description"` 39 | } 40 | 41 | func getProviderSchema(p *client.Provider) (ProviderSchema, error) { 42 | resp := p.GRPCProvider.GetSchema() 43 | ps := ProviderSchema{ 44 | Name: p.Name, 45 | Attributes: make(map[string]schemaAttribute), 46 | } 47 | if resp.Diagnostics.HasErrors() { 48 | return ps, resp.Diagnostics.NonFatalErr() 49 | } 50 | cfgSchema := resp.Provider.Block 51 | for key, attr := range cfgSchema.Attributes { 52 | ps.Attributes[key] = schemaAttribute{ 53 | Type: attr.Type.FriendlyName(), 54 | Required: attr.Required, 55 | Optional: attr.Optional, 56 | Computed: attr.Computed, 57 | Description: attr.Description, 58 | } 59 | //fmt.Printf("%s : type=%s, required=%b, optional=%b, computed=%b, description=%s\n", key, attr.Type.FriendlyName(), attr.Required, attr.Optional, attr.Computed, attr.Description) 60 | // Attribute represents a configuration attribute, within a block. 61 | } 62 | return ps, nil 63 | } 64 | -------------------------------------------------------------------------------- /cmd/template-compile/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/alecthomas/kong" 5 | "github.com/crossplane-contrib/terraform-provider-gen/internal/template" 6 | "github.com/spf13/afero" 7 | ) 8 | 9 | var CompileTemplate struct { 10 | Root string `help:"path to terraform-provider-gen repository root (default CWD)."` 11 | Output string `help:"Compiled templates will be written to this directory."` 12 | PackageRoot string `help:"Go package that compiled templates will exist within."` 13 | } 14 | 15 | func main() { 16 | ctx := kong.Parse(&CompileTemplate) 17 | ctx.FatalIfErrorf(compileTemplates(CompileTemplate.Root, CompileTemplate.Output, CompileTemplate.PackageRoot)) 18 | } 19 | 20 | func compileTemplates(root, output, packageRoot string) error { 21 | templateDir := "hack/template" 22 | tc := template.NewTemplateCompiler(afero.NewOsFs(), root, templateDir, output, packageRoot) 23 | return tc.CompileGeneratedTemplates() 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crossplane-contrib/terraform-provider-gen 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/alecthomas/kong v0.2.11 7 | github.com/crossplane-contrib/terraform-runtime v0.0.0-20210317191104-9eb36dba841c 8 | github.com/dave/jennifer v1.3.0 9 | github.com/hashicorp/terraform v0.13.5 10 | github.com/iancoleman/strcase v0.1.2 11 | github.com/pkg/errors v0.9.1 12 | github.com/spf13/afero v1.4.1 13 | github.com/zclconf/go-cty v1.7.0 14 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 15 | sigs.k8s.io/yaml v1.2.0 16 | ) 17 | 18 | replace ( 19 | k8s.io/api => k8s.io/api v0.18.6 20 | k8s.io/apimachinery => k8s.io/apimachinery v0.18.6 21 | k8s.io/client-go => k8s.io/client-go v0.18.6 22 | ) 23 | -------------------------------------------------------------------------------- /hack/template/pkg/generator/compare.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "github.com/crossplane/crossplane-runtime/pkg/resource" 21 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 22 | ) 23 | 24 | {{ .Mergers }} -------------------------------------------------------------------------------- /hack/template/pkg/generator/configure.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package {{ .KubernetesVersion}} 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/crossplane/crossplane-runtime/pkg/event" 23 | "github.com/crossplane/crossplane-runtime/pkg/logging" 24 | "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 25 | "github.com/crossplane/crossplane-runtime/pkg/resource" 26 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 27 | "github.com/crossplane-contrib/terraform-runtime/pkg/controller" 28 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | ) 31 | 32 | type reconcilerConfigurer struct{} 33 | 34 | // ConfigureReconciler adds a controller that reconciles the autogenerated managed.Resources in this package 35 | func (c *reconcilerConfigurer) ConfigureReconciler(mgr ctrl.Manager, l logging.Logger, idx *plugin.Index, pool *client.ProviderPool) error { 36 | name := managed.ControllerName(GroupKind) 37 | r := managed.NewReconciler(mgr, 38 | resource.ManagedKind(GroupVersionKind), 39 | managed.WithInitializers(), 40 | managed.WithTimeout(time.Duration(3600*time.Second)), 41 | managed.WithExternalConnecter(&controller.Connector{KubeClient: mgr.GetClient(), PluginIndex: idx, Logger: l, Pool: pool}), 42 | managed.WithLogger(l.WithValues("controller", name)), 43 | managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) 44 | 45 | return ctrl.NewControllerManagedBy(mgr). 46 | Named(name). 47 | For(&{{ .ManagedResourceName }}{}). 48 | Complete(r) 49 | } 50 | -------------------------------------------------------------------------------- /hack/template/pkg/generator/decode.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/crossplane/crossplane-runtime/pkg/meta" 23 | "github.com/crossplane/crossplane-runtime/pkg/resource" 24 | "github.com/hashicorp/terraform/providers" 25 | "github.com/zclconf/go-cty/cty" 26 | ctwhy "github.com/crossplane-contrib/terraform-runtime/pkg/plugin/cty" 27 | ) 28 | 29 | {{ .Decoders}} -------------------------------------------------------------------------------- /hack/template/pkg/generator/doc.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package {{ .KubernetesVersion}} 18 | 19 | // +kubebuilder:object:generate=true 20 | // +kubebuilder:validation:Optional 21 | // +groupName={{ .APIGroup }} 22 | // +versionName={{ .KubernetesVersion }} 23 | -------------------------------------------------------------------------------- /hack/template/pkg/generator/encode.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/zclconf/go-cty/cty" 23 | "github.com/crossplane/crossplane-runtime/pkg/meta" 24 | "github.com/crossplane/crossplane-runtime/pkg/resource" 25 | "github.com/hashicorp/terraform/providers" 26 | ) 27 | 28 | {{ .Encoders}} -------------------------------------------------------------------------------- /hack/template/pkg/generator/index.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | // Package type metadata. 26 | const ( 27 | Group = "{{ .APIGroup }}" 28 | Version = "{{ .APIVersion }}" 29 | ) 30 | 31 | var ( 32 | // SchemeGroupVersion is group version used to register these objects 33 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 34 | ) 35 | 36 | var ( 37 | Kind = "{{ .ManagedResourceName }}" 38 | GroupKind = schema.GroupKind{Group: Group, Kind: Kind}.String() 39 | KindAPIVersion = Kind + "." + SchemeGroupVersion.String() 40 | GroupVersionKind = SchemeGroupVersion.WithKind(Kind) 41 | TerraformResourceName = "{{ .TerraformResourceName }}" 42 | ) 43 | 44 | func Implementation() *plugin.Implementation { 45 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 46 | schemeBuilder := &scheme.Builder{GroupVersion: SchemeGroupVersion} 47 | schemeBuilder.Register(&{{ .ManagedResourceName }}{}, &{{ .ManagedResourceListName }}{}) 48 | return &plugin.Implementation{ 49 | GVK: GroupVersionKind, 50 | TerraformResourceName: TerraformResourceName, 51 | SchemeBuilder: schemeBuilder, 52 | ReconcilerConfigurer: &reconcilerConfigurer{}, 53 | ResourceMerger: &resourceMerger{}, 54 | CtyEncoder: &ctyEncoder{}, 55 | CtyDecoder: &ctyDecoder{}, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hack/template/pkg/generator/types.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | {{- .TypeDefs}} -------------------------------------------------------------------------------- /hack/template/pkg/template/test-template-getter.txt: -------------------------------------------------------------------------------- 1 | see tests for TemplateGetter in pkg/template to understand why this file is here. 2 | -------------------------------------------------------------------------------- /hack/template/provider/cmd/provider/main.go.tpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | 23 | "gopkg.in/alecthomas/kingpin.v2" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 26 | 27 | "{{ .RootPackage }}/generated" 28 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 29 | "github.com/crossplane/crossplane-runtime/pkg/logging" 30 | 31 | "github.com/crossplane-contrib/terraform-runtime/pkg/controller" 32 | ) 33 | 34 | func main() { 35 | var ( 36 | app = kingpin.New(filepath.Base(os.Args[0]), "Template support for Crossplane.").DefaultEnvars() 37 | debug = app.Flag("debug", "Run with debug logging.").Short('d').Bool() 38 | syncPeriod = app.Flag("sync", "Controller manager sync period such as 300ms, 1.5h, or 2h45m").Short('s').Default("1h").Duration() 39 | // TODO: pluginPath should receive a default pointing to a canonical path in a container generated by the build process 40 | pluginPath = app.Flag("pluginPath", "Full path to directory where terraform plugin is located.").String() 41 | ) 42 | kingpin.MustParse(app.Parse(os.Args[1:])) 43 | 44 | zl := zap.New(zap.UseDevMode(*debug)) 45 | log := logging.NewLogrLogger(zl.WithName("terraform-runtime")) 46 | if *debug { 47 | // The controller-runtime runs with a no-op logger by default. It is 48 | // *very* verbose even at info level, so we only provide it a real 49 | // logger when we're running in debug mode. 50 | ctrl.SetLogger(zl) 51 | } 52 | 53 | providerInit := generated.ProviderInit() 54 | idx, err := generated.Index() 55 | if err != nil { 56 | kingpin.FatalIfError(err, "Failed to build index of generated code (generated.Index())") 57 | } 58 | 59 | opts := ctrl.Options{SyncPeriod: syncPeriod} 60 | ropts := client.NewRuntimeOptions(). 61 | WithPluginPath(*pluginPath). 62 | WithPoolSize(5) 63 | log.Debug("Starting", "sync-period", syncPeriod.String()) 64 | 65 | err = controller.StartTerraformManager(idx, providerInit, opts, ropts, log) 66 | kingpin.FatalIfError(err, "Cannot start the generated terraform controllers") 67 | } 68 | -------------------------------------------------------------------------------- /hack/template/provider/generated/index.go.tpl: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import ( 4 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 5 | ) 6 | 7 | var resourceImplementations = make([]*plugin.Implementation, 0) 8 | var providerInit *plugin.ProviderInit 9 | 10 | // Index provides a plugin.Index for the generated provider 11 | // note that the value of resourceImplementations is populated 12 | // at runtime in implementations.go. This is to enable the separation of 13 | // the build into multiple stages. 14 | func Index() (*plugin.Index, error) { 15 | idxr := plugin.NewIndexer() 16 | for _, impl := range resourceImplementations { 17 | err := idxr.Overlay(impl) 18 | if err != nil { 19 | return nil, err 20 | } 21 | } 22 | return idxr.BuildIndex() 23 | } 24 | 25 | // Index provides a plugin.ProviderInit for the generated provider. 26 | // Note that the value of providerInit is populated 27 | // at runtime in provider.go. This is to enable the separation of 28 | // the build into multiple stages. 29 | func ProviderInit() *plugin.ProviderInit { 30 | return providerInit 31 | } 32 | -------------------------------------------------------------------------------- /hack/template/provider/generated/index_provider.go.tpl: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import ( 4 | "{{ .RootPackage }}/generated/provider/{{ .ProviderConfigVersion }}" 5 | ) 6 | 7 | func init() { 8 | providerInit = {{ .ProviderConfigVersion }}.GetProviderInit() 9 | } 10 | -------------------------------------------------------------------------------- /hack/template/provider/generated/index_resources.go.tpl: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | import ( 4 | {{- range .PackageImports }} 5 | {{ .Name }} "{{ .Path }}" 6 | {{- end }} 7 | 8 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 9 | ) 10 | 11 | var generatedImplementations = []*plugin.Implementation{ 12 | {{- range .PackageImports }} 13 | {{ .Name }}.Implementation(), 14 | {{- end}} 15 | } 16 | 17 | // this is deferred until init time to simplify the codegen workflow. 18 | // index.go can be a simple templated, satisfying the needs of main.go so that 19 | // the provider can be compiled (albeit in a non-functional state) enabling angryjet 20 | // and controller-gen to run against the generated types.go before the a subsequent pass 21 | // of terraform-provider-gen adds the compare/encode/decode methods. 22 | func init() { 23 | for _, impl := range generatedImplementations { 24 | resourceImplementations = append(resourceImplementations, impl) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /hack/template/provider/generated/provider/v1alpha1/doc.go.tpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains the core resources of the Google Cloud Platform. 18 | // +kubebuilder:object:generate=true 19 | // +groupName={{ .APIGroup }} 20 | // +versionName=v1alpha1 21 | package v1alpha1 22 | -------------------------------------------------------------------------------- /hack/template/provider/generated/provider/v1alpha1/index.go.tpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | "reflect" 25 | 26 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 27 | "github.com/crossplane-contrib/terraform-runtime/pkg/plugin" 28 | "github.com/crossplane/crossplane-runtime/pkg/resource" 29 | "github.com/pkg/errors" 30 | "github.com/zclconf/go-cty/cty" 31 | "k8s.io/apimachinery/pkg/runtime/schema" 32 | "k8s.io/apimachinery/pkg/types" 33 | kubeclient "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/scheme" 35 | ) 36 | 37 | // Package type metadata. 38 | const ( 39 | Group = "vsphere.terraform-plugin.crossplane.io" 40 | Version = "v1alpha1" 41 | ProviderName = "vsphere" 42 | ) 43 | 44 | var ( 45 | // SchemeGroupVersion is group version used to register these objects 46 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 47 | // Provider type metadata. 48 | ProviderKind = reflect.TypeOf(ProviderConfig{}).Name() 49 | ProviderGroupKind = schema.GroupKind{Group: Group, Kind: ProviderKind}.String() 50 | ProviderKindAPIVersion = ProviderKind + "." + SchemeGroupVersion.String() 51 | ProviderGroupVersionKind = SchemeGroupVersion.WithKind(ProviderKind) 52 | ) 53 | 54 | func initializeProvider(ctx context.Context, mr resource.Managed, ropts *client.RuntimeOptions, kube kubeclient.Client) (*client.Provider, error) { 55 | pc := &ProviderConfig{} 56 | if err := kube.Get(ctx, types.NamespacedName{Name: mr.GetProviderConfigReference().Name}, pc); err != nil { 57 | return nil, errors.Wrap(err, "cannot get referenced Provider") 58 | } 59 | 60 | t := resource.NewProviderConfigUsageTracker(kube, &ProviderConfigUsage{}) 61 | if err := t.Track(ctx, mr); err != nil { 62 | return nil, errors.Wrap(err, "cannot track ProviderConfig usage") 63 | } 64 | 65 | pass, err := readPassword(ctx, kube, pc) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "cannot read credentials for ProviderConfig") 68 | } 69 | cfg := populateConfig(pc, pass) 70 | 71 | p, err := client.NewProvider(ProviderName, ropts.PluginPath) 72 | if err != nil { 73 | return p, err 74 | } 75 | err = p.Configure(cfg) 76 | return p, err 77 | } 78 | 79 | func populateConfig(p *ProviderConfig, password string) cty.Value { 80 | merged := make(map[string]cty.Value) 81 | merged["api_timeout"] = cty.NumberIntVal(p.Spec.ApiTimeout) 82 | merged["rest_session_path"] = cty.StringVal(p.Spec.RestSessionPath) 83 | merged["vcenter_server"] = cty.StringVal(p.Spec.VcenterServer) 84 | merged["vim_keep_alive"] = cty.NumberIntVal(p.Spec.VimKeepAlive) 85 | merged["allow_unverified_ssl"] = cty.BoolVal(p.Spec.AllowUnverifiedSsl) 86 | merged["client_debug"] = cty.BoolVal(p.Spec.ClientDebug) 87 | merged["client_debug_path"] = cty.StringVal(p.Spec.ClientDebugPath) 88 | merged["client_debug_path_run"] = cty.StringVal(p.Spec.ClientDebugPathRun) 89 | merged["persist_session"] = cty.BoolVal(p.Spec.PersistSession) 90 | merged["vim_session_path"] = cty.StringVal(p.Spec.VimSessionPath) 91 | merged["vsphere_server"] = cty.StringVal(p.Spec.VsphereServer) 92 | merged["user"] = cty.StringVal(p.Spec.User) 93 | 94 | merged["password"] = cty.StringVal(password) 95 | return cty.ObjectVal(merged) 96 | } 97 | 98 | func GetProviderInit() *plugin.ProviderInit { 99 | schemeBuilder := &scheme.Builder{GroupVersion: SchemeGroupVersion} 100 | schemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{}) 101 | schemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{}) 102 | return &plugin.ProviderInit{ 103 | SchemeBuilder: schemeBuilder, 104 | Initializer: initializeProvider, 105 | } 106 | } 107 | 108 | func readPassword(ctx context.Context, kube kubeclient.Client, pc *ProviderConfig) (string, error) { 109 | if s := pc.Spec.Credentials.Source; s != xpv1.CredentialsSourceSecret { 110 | return "", errors.Errorf("unsupported credentials source %q", s) 111 | } 112 | ref := pc.Spec.Credentials.SecretRef 113 | if ref == nil { 114 | return "", errors.New("no credentials secret reference was provided") 115 | } 116 | if ref.Key == "" { 117 | return "", fmt.Errorf("secret reference 'Key' field must be specified for ProviderConfig %s", pc.Name) 118 | } 119 | 120 | s := &corev1.Secret{} 121 | if err := kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace}, s); err != nil { 122 | return "", err 123 | } 124 | 125 | password, ok := s.Data[ref.Key] 126 | if !ok || len(password) == 0 { 127 | return "", fmt.Errorf("Cannot read value from password key (%s) of secret at %s.%s", ref.Key, ref.Namespace, ref.Name) 128 | } 129 | 130 | return string(password), nil 131 | } 132 | -------------------------------------------------------------------------------- /hack/template/provider/generated/provider/v1alpha1/types.go.tpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | 26 | // TODO: generate this -- currently hardcoded for vsphere, which should move to an overlay until we have generation 27 | // support for ProviderConfigs 28 | 29 | // A ProviderConfigSpec defines the desired state of a ProviderConfig. 30 | type ProviderConfigSpec struct { 31 | xpv1.ProviderConfigSpec `json:",inline"` 32 | ApiTimeout int64 `json:"api_timeout,omitempty"` 33 | RestSessionPath string `json:"rest_session_path,omitempty"` 34 | VcenterServer string `json:"vcenter_server,omitempty"` 35 | VimKeepAlive int64 `json:"vim_keep_alive,omitempty"` 36 | AllowUnverifiedSsl bool `json:"allow_unverified_ssl,omitempty"` 37 | ClientDebug bool `json:"client_debug,omitempty"` 38 | ClientDebugPath string `json:"client_debug_path,omitempty"` 39 | ClientDebugPathRun string `json:"client_debug_path_run,omitempty"` 40 | // +kubebuilder:validation:Required 41 | Password string `json:"password"` 42 | PersistSession bool `json:"persist_session,omitempty"` 43 | // +kubebuilder:validation:Required 44 | User string `json:"user"` 45 | VimSessionPath string `json:"vim_session_path,omitempty"` 46 | VsphereServer string `json:"vsphere_server,omitempty"` 47 | } 48 | 49 | // A ProviderConfigStatus represents the status of a ProviderConfig. 50 | type ProviderConfigStatus struct { 51 | xpv1.ProviderConfigStatus `json:",inline"` 52 | } 53 | 54 | // +kubebuilder:object:root=true 55 | 56 | // A ProviderConfig configures how controllers will connect to a provider's API. 57 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 58 | // +kubebuilder:printcolumn:name="SECRET-NAME",type="string",JSONPath=".spec.credentialsSecretRef.name",priority=1 59 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,{{ .Name }}} 60 | // +kubebuilder:subresource:status 61 | type ProviderConfig struct { 62 | metav1.TypeMeta `json:",inline"` 63 | metav1.ObjectMeta `json:"metadata,omitempty"` 64 | 65 | Spec ProviderConfigSpec `json:"spec"` 66 | Status ProviderConfigStatus `json:"status,omitempty"` 67 | } 68 | 69 | // +kubebuilder:object:root=true 70 | 71 | // ProviderConfigList contains a list of ProviderConfig 72 | type ProviderConfigList struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ListMeta `json:"metadata,omitempty"` 75 | Items []ProviderConfig `json:"items"` 76 | } 77 | 78 | // +kubebuilder:object:root=true 79 | 80 | // A ProviderConfigUsage indicates that a resource is using a ProviderConfig. 81 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 82 | // +kubebuilder:printcolumn:name="CONFIG-NAME",type="string",JSONPath=".providerConfigRef.name" 83 | // +kubebuilder:printcolumn:name="RESOURCE-KIND",type="string",JSONPath=".resourceRef.kind" 84 | // +kubebuilder:printcolumn:name="RESOURCE-NAME",type="string",JSONPath=".resourceRef.name" 85 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,{{ .Name }}} 86 | type ProviderConfigUsage struct { 87 | metav1.TypeMeta `json:",inline"` 88 | metav1.ObjectMeta `json:"metadata,omitempty"` 89 | 90 | xpv1.ProviderConfigUsage `json:",inline"` 91 | } 92 | 93 | // +kubebuilder:object:root=true 94 | 95 | // ProviderConfigUsageList contains a list of ProviderConfigUsage 96 | type ProviderConfigUsageList struct { 97 | metav1.TypeMeta `json:",inline"` 98 | metav1.ListMeta `json:"metadata,omitempty"` 99 | Items []ProviderConfigUsage `json:"items"` 100 | } 101 | -------------------------------------------------------------------------------- /internal/template/compiled/dispatch.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | pkgGenerator "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled/pkg/generator" 5 | pkgTemplate "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled/pkg/template" 6 | providerCmdProvider "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled/provider/cmd/provider" 7 | providerGenerated "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled/provider/generated" 8 | providerGeneratedProviderV1Alpha1 "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled/provider/generated/provider/v1alpha1" 9 | ) 10 | 11 | var TemplateDispatchMap map[string]func() string = map[string]func() string{ 12 | "pkg/generator/compare.go.tmpl": pkgGenerator.Compare, 13 | "pkg/generator/configure.go.tmpl": pkgGenerator.Configure, 14 | "pkg/generator/decode.go.tmpl": pkgGenerator.Decode, 15 | "pkg/generator/doc.go.tmpl": pkgGenerator.Doc, 16 | "pkg/generator/encode.go.tmpl": pkgGenerator.Encode, 17 | "pkg/generator/index.go.tmpl": pkgGenerator.Index, 18 | "pkg/generator/types.go.tmpl": pkgGenerator.Types, 19 | "pkg/template/test-template-getter.txt": pkgTemplate.TestTemplateGetter, 20 | "provider/cmd/provider/main.go.tpl": providerCmdProvider.Main, 21 | "provider/generated/index.go.tpl": providerGenerated.Index, 22 | "provider/generated/index_provider.go.tpl": providerGenerated.IndexProvider, 23 | "provider/generated/index_resources.go.tpl": providerGenerated.IndexResources, 24 | "provider/generated/provider/v1alpha1/doc.go.tpl": providerGeneratedProviderV1Alpha1.Doc, 25 | "provider/generated/provider/v1alpha1/index.go.tpl": providerGeneratedProviderV1Alpha1.Index, 26 | "provider/generated/provider/v1alpha1/types.go.tpl": providerGeneratedProviderV1Alpha1.Types, 27 | } 28 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/compare.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Compare() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\t\"github.com/crossplane/crossplane-runtime/pkg/resource\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n)\n\n{{ .Mergers }}" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/configure.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Configure() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage {{ .KubernetesVersion}}\n\nimport (\n\t\"time\"\n\n\t\"github.com/crossplane/crossplane-runtime/pkg/event\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/logging\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/resource\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/client\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/controller\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n)\n\ntype reconcilerConfigurer struct{}\n\n// ConfigureReconciler adds a controller that reconciles the autogenerated managed.Resources in this package\nfunc (c *reconcilerConfigurer) ConfigureReconciler(mgr ctrl.Manager, l logging.Logger, idx *plugin.Index, pool *client.ProviderPool) error {\n\tname := managed.ControllerName(GroupKind)\n\tr := managed.NewReconciler(mgr,\n\t\tresource.ManagedKind(GroupVersionKind),\n\t\tmanaged.WithInitializers(),\n\t\tmanaged.WithTimeout(time.Duration(3600*time.Second)),\n\t\tmanaged.WithExternalConnecter(&controller.Connector{KubeClient: mgr.GetClient(), PluginIndex: idx, Logger: l, Pool: pool}),\n\t\tmanaged.WithLogger(l.WithValues(\"controller\", name)),\n\t\tmanaged.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))\n\n\treturn ctrl.NewControllerManagedBy(mgr).\n\t\tNamed(name).\n\t\tFor(&{{ .ManagedResourceName }}{}).\n\t\tComplete(r)\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/decode.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Decode() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/crossplane/crossplane-runtime/pkg/meta\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/resource\"\n\t\"github.com/hashicorp/terraform/providers\"\n\t\"github.com/zclconf/go-cty/cty\"\n\tctwhy \"github.com/crossplane-contrib/terraform-runtime/pkg/plugin/cty\"\n)\n\n{{ .Decoders}}" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/doc.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Doc() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage {{ .KubernetesVersion}}\n\n// +kubebuilder:object:generate=true\n// +kubebuilder:validation:Optional\n// +groupName={{ .APIGroup }}\n// +versionName={{ .KubernetesVersion }}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/encode.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Encode() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/meta\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/resource\"\n\t\"github.com/hashicorp/terraform/providers\"\n)\n\n{{ .Encoders}}" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/index.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Index() string { 4 | return "/*\nCopyright 2019 The Crossplane Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"sigs.k8s.io/controller-runtime/pkg/scheme\"\n)\n\n// Package type metadata.\nconst (\n\tGroup = \"{{ .APIGroup }}\"\n\tVersion = \"{{ .APIVersion }}\"\n)\n\nvar (\n\t// SchemeGroupVersion is group version used to register these objects\n\tSchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}\n)\n\nvar (\n\tKind = \"{{ .ManagedResourceName }}\"\n\tGroupKind = schema.GroupKind{Group: Group, Kind: Kind}.String()\n\tKindAPIVersion = Kind + \".\" + SchemeGroupVersion.String()\n\tGroupVersionKind = SchemeGroupVersion.WithKind(Kind)\n\tTerraformResourceName = \"{{ .TerraformResourceName }}\"\n)\n\nfunc Implementation() *plugin.Implementation {\n\t// SchemeBuilder is used to add go types to the GroupVersionKind scheme\n\tschemeBuilder := &scheme.Builder{GroupVersion: SchemeGroupVersion}\n\tschemeBuilder.Register(&{{ .ManagedResourceName }}{}, &{{ .ManagedResourceListName }}{})\n\treturn &plugin.Implementation{\n\t\tGVK: GroupVersionKind,\n\t\tTerraformResourceName: TerraformResourceName,\n\t\tSchemeBuilder: schemeBuilder,\n\t\tReconcilerConfigurer: &reconcilerConfigurer{},\n\t\tResourceMerger: &resourceMerger{},\n\t\tCtyEncoder: &ctyEncoder{},\n\t\tCtyDecoder: &ctyDecoder{},\n\t}\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/generator/types.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | func Types() string { 4 | return "/*\n\tCopyright 2019 The Crossplane Authors.\n\n\tLicensed under the Apache License, Version 2.0 (the \"License\");\n\tyou may not use this file except in compliance with the License.\n\tYou may obtain a copy of the License at\n\n\t http://www.apache.org/licenses/LICENSE-2.0\n\n\tUnless required by applicable law or agreed to in writing, software\n\tdistributed under the License is distributed on an \"AS IS\" BASIS,\n\tWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\tSee the License for the specific language governing permissions and\n\tlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\txpv1 \"github.com/crossplane/crossplane-runtime/apis/common/v1\"\n)\n{{- .TypeDefs}}" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/pkg/template/test_template_getter.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | func TestTemplateGetter() string { 4 | return "see tests for TemplateGetter in pkg/template to understand why this file is here.\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/cmd/provider/main.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | func Main() string { 4 | return "/*\nCopyright 2020 The Crossplane Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"gopkg.in/alecthomas/kingpin.v2\"\n\tctrl \"sigs.k8s.io/controller-runtime\"\n\t\"sigs.k8s.io/controller-runtime/pkg/log/zap\"\n\n\t\"{{ .RootPackage }}/generated\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/client\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/logging\"\n\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/controller\"\n)\n\nfunc main() {\n\tvar (\n\t\tapp = kingpin.New(filepath.Base(os.Args[0]), \"Template support for Crossplane.\").DefaultEnvars()\n\t\tdebug = app.Flag(\"debug\", \"Run with debug logging.\").Short('d').Bool()\n\t\tsyncPeriod = app.Flag(\"sync\", \"Controller manager sync period such as 300ms, 1.5h, or 2h45m\").Short('s').Default(\"1h\").Duration()\n\t\t// TODO: pluginPath should receive a default pointing to a canonical path in a container generated by the build process\n\t\tpluginPath = app.Flag(\"pluginPath\", \"Full path to directory where terraform plugin is located.\").String()\n\t)\n\tkingpin.MustParse(app.Parse(os.Args[1:]))\n\n\tzl := zap.New(zap.UseDevMode(*debug))\n\tlog := logging.NewLogrLogger(zl.WithName(\"terraform-runtime\"))\n\tif *debug {\n\t\t// The controller-runtime runs with a no-op logger by default. It is\n\t\t// *very* verbose even at info level, so we only provide it a real\n\t\t// logger when we're running in debug mode.\n\t\tctrl.SetLogger(zl)\n\t}\n\n\tproviderInit := generated.ProviderInit()\n\tidx, err := generated.Index()\n\tif err != nil {\n\t\tkingpin.FatalIfError(err, \"Failed to build index of generated code (generated.Index())\")\n\t}\n\n\topts := ctrl.Options{SyncPeriod: syncPeriod}\n\tropts := client.NewRuntimeOptions().\n\t\tWithPluginPath(*pluginPath).\n\t\tWithPoolSize(5)\n\tlog.Debug(\"Starting\", \"sync-period\", syncPeriod.String())\n\n\terr = controller.StartTerraformManager(idx, providerInit, opts, ropts, log)\n\tkingpin.FatalIfError(err, \"Cannot start the generated terraform controllers\")\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/index.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | func Index() string { 4 | return "package generated\n\nimport (\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n)\n\nvar resourceImplementations = make([]*plugin.Implementation, 0)\nvar providerInit *plugin.ProviderInit\n\n// Index provides a plugin.Index for the generated provider\n// note that the value of resourceImplementations is populated\n// at runtime in implementations.go. This is to enable the separation of\n// the build into multiple stages.\nfunc Index() (*plugin.Index, error) {\n\tidxr := plugin.NewIndexer()\n\tfor _, impl := range resourceImplementations {\n\t\terr := idxr.Overlay(impl)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn idxr.BuildIndex()\n}\n\n// Index provides a plugin.ProviderInit for the generated provider.\n// Note that the value of providerInit is populated\n// at runtime in provider.go. This is to enable the separation of\n// the build into multiple stages.\nfunc ProviderInit() *plugin.ProviderInit {\n\treturn providerInit\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/index_provider.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | func IndexProvider() string { 4 | return "package generated\n\nimport (\n \"{{ .RootPackage }}/generated/provider/{{ .ProviderConfigVersion }}\"\n)\n\nfunc init() {\n providerInit = {{ .ProviderConfigVersion }}.GetProviderInit()\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/index_resources.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | func IndexResources() string { 4 | return "package generated\n\nimport (\n{{- range .PackageImports }}\n {{ .Name }} \"{{ .Path }}\"\n{{- end }}\n\n \"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n)\n\nvar generatedImplementations = []*plugin.Implementation{\n{{- range .PackageImports }}\n {{ .Name }}.Implementation(),\n{{- end}}\n}\n\n// this is deferred until init time to simplify the codegen workflow.\n// index.go can be a simple templated, satisfying the needs of main.go so that\n// the provider can be compiled (albeit in a non-functional state) enabling angryjet\n// and controller-gen to run against the generated types.go before the a subsequent pass\n// of terraform-provider-gen adds the compare/encode/decode methods.\nfunc init() {\n for _, impl := range generatedImplementations {\n resourceImplementations = append(resourceImplementations, impl)\n }\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/provider/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | func Doc() string { 4 | return "/*\nCopyright 2019 The Crossplane Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package v1alpha1 contains the core resources of the Google Cloud Platform.\n// +kubebuilder:object:generate=true\n// +groupName={{ .APIGroup }}\n// +versionName=v1alpha1\npackage v1alpha1\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/provider/v1alpha1/index.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | func Index() string { 4 | return "/*\nCopyright 2019 The Crossplane Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\txpv1 \"github.com/crossplane/crossplane-runtime/apis/common/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"reflect\"\n\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/client\"\n\t\"github.com/crossplane-contrib/terraform-runtime/pkg/plugin\"\n\t\"github.com/crossplane/crossplane-runtime/pkg/resource\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\tkubeclient \"sigs.k8s.io/controller-runtime/pkg/client\"\n\t\"sigs.k8s.io/controller-runtime/pkg/scheme\"\n)\n\n// Package type metadata.\nconst (\n\tGroup = \"vsphere.terraform-plugin.crossplane.io\"\n\tVersion = \"v1alpha1\"\n\tProviderName = \"vsphere\"\n)\n\nvar (\n\t// SchemeGroupVersion is group version used to register these objects\n\tSchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}\n\t// Provider type metadata.\n\tProviderKind = reflect.TypeOf(ProviderConfig{}).Name()\n\tProviderGroupKind = schema.GroupKind{Group: Group, Kind: ProviderKind}.String()\n\tProviderKindAPIVersion = ProviderKind + \".\" + SchemeGroupVersion.String()\n\tProviderGroupVersionKind = SchemeGroupVersion.WithKind(ProviderKind)\n)\n\nfunc initializeProvider(ctx context.Context, mr resource.Managed, ropts *client.RuntimeOptions, kube kubeclient.Client) (*client.Provider, error) {\n\tpc := &ProviderConfig{}\n\tif err := kube.Get(ctx, types.NamespacedName{Name: mr.GetProviderConfigReference().Name}, pc); err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot get referenced Provider\")\n\t}\n\n\tt := resource.NewProviderConfigUsageTracker(kube, &ProviderConfigUsage{})\n\tif err := t.Track(ctx, mr); err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot track ProviderConfig usage\")\n\t}\n\n\tpass, err := readPassword(ctx, kube, pc)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cannot read credentials for ProviderConfig\")\n\t}\n\tcfg := populateConfig(pc, pass)\n\n\tp, err := client.NewProvider(ProviderName, ropts.PluginPath)\n\tif err != nil {\n\t\treturn p, err\n\t}\n\terr = p.Configure(cfg)\n\treturn p, err\n}\n\nfunc populateConfig(p *ProviderConfig, password string) cty.Value {\n\tmerged := make(map[string]cty.Value)\n\tmerged[\"api_timeout\"] = cty.NumberIntVal(p.Spec.ApiTimeout)\n\tmerged[\"rest_session_path\"] = cty.StringVal(p.Spec.RestSessionPath)\n\tmerged[\"vcenter_server\"] = cty.StringVal(p.Spec.VcenterServer)\n\tmerged[\"vim_keep_alive\"] = cty.NumberIntVal(p.Spec.VimKeepAlive)\n\tmerged[\"allow_unverified_ssl\"] = cty.BoolVal(p.Spec.AllowUnverifiedSsl)\n\tmerged[\"client_debug\"] = cty.BoolVal(p.Spec.ClientDebug)\n\tmerged[\"client_debug_path\"] = cty.StringVal(p.Spec.ClientDebugPath)\n\tmerged[\"client_debug_path_run\"] = cty.StringVal(p.Spec.ClientDebugPathRun)\n\tmerged[\"persist_session\"] = cty.BoolVal(p.Spec.PersistSession)\n\tmerged[\"vim_session_path\"] = cty.StringVal(p.Spec.VimSessionPath)\n\tmerged[\"vsphere_server\"] = cty.StringVal(p.Spec.VsphereServer)\n\tmerged[\"user\"] = cty.StringVal(p.Spec.User)\n\n\tmerged[\"password\"] = cty.StringVal(password)\n\treturn cty.ObjectVal(merged)\n}\n\nfunc GetProviderInit() *plugin.ProviderInit {\n\tschemeBuilder := &scheme.Builder{GroupVersion: SchemeGroupVersion}\n\tschemeBuilder.Register(&ProviderConfig{}, &ProviderConfigList{})\n\tschemeBuilder.Register(&ProviderConfigUsage{}, &ProviderConfigUsageList{})\n\treturn &plugin.ProviderInit{\n\t\tSchemeBuilder: schemeBuilder,\n\t\tInitializer: initializeProvider,\n\t}\n}\n\nfunc readPassword(ctx context.Context, kube kubeclient.Client, pc *ProviderConfig) (string, error) {\n\tif s := pc.Spec.Credentials.Source; s != xpv1.CredentialsSourceSecret {\n\t\treturn \"\", errors.Errorf(\"unsupported credentials source %q\", s)\n\t}\n\tref := pc.Spec.Credentials.SecretRef\n\tif ref == nil {\n\t\treturn \"\", errors.New(\"no credentials secret reference was provided\")\n\t}\n\tif ref.Key == \"\" {\n\t\treturn \"\", fmt.Errorf(\"secret reference 'Key' field must be specified for ProviderConfig %s\", pc.Name)\n\t}\n\n\ts := &corev1.Secret{}\n\tif err := kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace}, s); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tpassword, ok := s.Data[ref.Key]\n\tif !ok || len(password) == 0 {\n\t\treturn \"\", fmt.Errorf(\"Cannot read value from password key (%s) of secret at %s.%s\", ref.Key, ref.Namespace, ref.Name)\n\t}\n\n\treturn string(password), nil\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/compiled/provider/generated/provider/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | func Types() string { 4 | return "/*\nCopyright 2020 The Crossplane Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage v1alpha1\n\nimport (\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\txpv1 \"github.com/crossplane/crossplane-runtime/apis/common/v1\"\n)\n\n\n// TODO: generate this -- currently hardcoded for vsphere, which should move to an overlay until we have generation\n// support for ProviderConfigs\n\n// A ProviderConfigSpec defines the desired state of a ProviderConfig.\ntype ProviderConfigSpec struct {\n\txpv1.ProviderConfigSpec `json:\",inline\"`\n\tApiTimeout int64 `json:\"api_timeout,omitempty\"`\n\tRestSessionPath string `json:\"rest_session_path,omitempty\"`\n\tVcenterServer string `json:\"vcenter_server,omitempty\"`\n\tVimKeepAlive int64 `json:\"vim_keep_alive,omitempty\"`\n\tAllowUnverifiedSsl bool `json:\"allow_unverified_ssl,omitempty\"`\n\tClientDebug bool `json:\"client_debug,omitempty\"`\n\tClientDebugPath string `json:\"client_debug_path,omitempty\"`\n\tClientDebugPathRun string `json:\"client_debug_path_run,omitempty\"`\n\t// +kubebuilder:validation:Required\n\tPassword string `json:\"password\"`\n\tPersistSession bool `json:\"persist_session,omitempty\"`\n\t// +kubebuilder:validation:Required\n\tUser string `json:\"user\"`\n\tVimSessionPath string `json:\"vim_session_path,omitempty\"`\n\tVsphereServer string `json:\"vsphere_server,omitempty\"`\n}\n\n// A ProviderConfigStatus represents the status of a ProviderConfig.\ntype ProviderConfigStatus struct {\n\txpv1.ProviderConfigStatus `json:\",inline\"`\n}\n\n// +kubebuilder:object:root=true\n\n// A ProviderConfig configures how controllers will connect to a provider's API.\n// +kubebuilder:printcolumn:name=\"AGE\",type=\"date\",JSONPath=\".metadata.creationTimestamp\"\n// +kubebuilder:printcolumn:name=\"SECRET-NAME\",type=\"string\",JSONPath=\".spec.credentialsSecretRef.name\",priority=1\n// +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,{{ .Name }}}\n// +kubebuilder:subresource:status\ntype ProviderConfig struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n\tSpec ProviderConfigSpec `json:\"spec\"`\n\tStatus ProviderConfigStatus `json:\"status,omitempty\"`\n}\n\n// +kubebuilder:object:root=true\n\n// ProviderConfigList contains a list of ProviderConfig\ntype ProviderConfigList struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ListMeta `json:\"metadata,omitempty\"`\n\tItems []ProviderConfig `json:\"items\"`\n}\n\n// +kubebuilder:object:root=true\n\n// A ProviderConfigUsage indicates that a resource is using a ProviderConfig.\n// +kubebuilder:printcolumn:name=\"AGE\",type=\"date\",JSONPath=\".metadata.creationTimestamp\"\n// +kubebuilder:printcolumn:name=\"CONFIG-NAME\",type=\"string\",JSONPath=\".providerConfigRef.name\"\n// +kubebuilder:printcolumn:name=\"RESOURCE-KIND\",type=\"string\",JSONPath=\".resourceRef.kind\"\n// +kubebuilder:printcolumn:name=\"RESOURCE-NAME\",type=\"string\",JSONPath=\".resourceRef.name\"\n// +kubebuilder:resource:scope=Cluster,categories={crossplane,provider,{{ .Name }}}\ntype ProviderConfigUsage struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n\txpv1.ProviderConfigUsage `json:\",inline\"`\n}\n\n// +kubebuilder:object:root=true\n\n// ProviderConfigUsageList contains a list of ProviderConfigUsage\ntype ProviderConfigUsageList struct {\n\tmetav1.TypeMeta `json:\",inline\"`\n\tmetav1.ListMeta `json:\"metadata,omitempty\"`\n\tItems []ProviderConfigUsage `json:\"items\"`\n}\n" 5 | } 6 | -------------------------------------------------------------------------------- /internal/template/generator.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "github.com/dave/jennifer/jen" 5 | "github.com/iancoleman/strcase" 6 | "github.com/spf13/afero" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func (tc *TemplateCompiler) Walk(path string, info os.FileInfo, err error) error { 14 | if info.IsDir() { 15 | return nil 16 | } 17 | tc.sourcePaths = append(tc.sourcePaths, path) 18 | return nil 19 | } 20 | 21 | type TemplateCompiler struct { 22 | sourcePaths []string 23 | root string 24 | templateDir string 25 | output string 26 | packageRoot string 27 | fs afero.Fs 28 | } 29 | 30 | func NewTemplateCompiler(fs afero.Fs, root, templateDir, output, packageRoot string) *TemplateCompiler { 31 | return &TemplateCompiler{ 32 | fs: fs, 33 | root: root, 34 | templateDir: templateDir, 35 | output: output, 36 | packageRoot: packageRoot, 37 | sourcePaths: make([]string, 0), 38 | } 39 | } 40 | 41 | type DispatchEntry struct { 42 | RelativePath string 43 | RelativeImport string 44 | FuncName string 45 | PackageRoot string 46 | } 47 | 48 | func (de DispatchEntry) ImportAlias() string { 49 | parts := strings.Split(de.RelativeImport, string(os.PathSeparator)) 50 | return strcase.ToLowerCamel(strings.Join(parts, " ")) 51 | } 52 | 53 | func (de DispatchEntry) dispatchMapEntry() (jen.Code, jen.Code) { 54 | key := jen.Lit(de.RelativePath) 55 | val := jen.Qual(de.importPath(), de.FuncName) 56 | return key, val 57 | } 58 | 59 | func (tc *TemplateCompiler) CompileGeneratedTemplates() error { 60 | err := afero.Walk(tc.fs, filepath.Join(tc.root, tc.templateDir), tc.Walk) 61 | if err != nil { 62 | return err 63 | } 64 | dispatchers := make([]DispatchEntry, 0) 65 | for _, source := range tc.sourcePaths { 66 | t := NewTemplateRenderer(source, tc.root, tc.templateDir, tc.output, tc.packageRoot, tc.fs) 67 | err := t.WriteCompiled() 68 | if err != nil { 69 | return err 70 | } 71 | dispatchers = append(dispatchers, t.DispatchEntry()) 72 | } 73 | return tc.renderDispatchMap(dispatchers) 74 | } 75 | 76 | func (de DispatchEntry) importPath() string { 77 | return path.Join(de.PackageRoot, de.RelativeImport) 78 | } 79 | 80 | func (tc *TemplateCompiler) renderDispatchMap(dispatchers []DispatchEntry) error { 81 | f := jen.NewFile("dispatch") 82 | mapEntries := make(jen.Dict) 83 | for _, d := range dispatchers { 84 | importPath := d.importPath() 85 | alias := d.ImportAlias() 86 | f.ImportAlias(importPath, alias) 87 | k, v := d.dispatchMapEntry() 88 | mapEntries[k] = v 89 | } 90 | mapType := jen.Map(jen.String()).Func().Params().String() 91 | mapAssignment := jen.Op("=").Map(jen.String()).Func().Params().String().Values(mapEntries) 92 | f.Var().Id("TemplateDispatchMap").Add(mapType).Add(mapAssignment) 93 | fh, err := tc.fs.Create(filepath.Join(tc.output, "dispatch.go")) 94 | if err != nil { 95 | return err 96 | } 97 | defer fh.Close() 98 | return f.Render(fh) 99 | } 100 | -------------------------------------------------------------------------------- /internal/template/render.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "github.com/dave/jennifer/jen" 6 | "github.com/iancoleman/strcase" 7 | "github.com/spf13/afero" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | type TemplateRenderer interface { 15 | WriteCompiled() error 16 | DispatchEntry() DispatchEntry 17 | } 18 | 19 | type templateRenderer struct { 20 | path string 21 | root string 22 | templateDir string 23 | fs afero.Fs 24 | outputBase string 25 | packageRoot string 26 | } 27 | 28 | func (tr *templateRenderer) fileName() string { 29 | return strcase.ToSnake(tr.funcName()) + ".go" 30 | } 31 | 32 | func normalize(s string) string { 33 | s = strings.ReplaceAll(s, "-", "_") 34 | return s 35 | } 36 | 37 | func (tr *templateRenderer) funcName() string { 38 | _, fname := filepath.Split(tr.path) 39 | parts := strings.Split(fname, ".") 40 | return strcase.ToCamel(normalize(parts[0])) 41 | } 42 | 43 | func (tr *templateRenderer) packageName() string { 44 | dir, _ := filepath.Split(tr.path) 45 | dirs := strings.Split(dir, string(os.PathSeparator)) 46 | // the last element is always a quote because of the dangling slash 47 | if dirs[len(dirs)-1] == "" { 48 | return dirs[len(dirs)-2] 49 | } 50 | return dirs[len(dirs)-1] 51 | } 52 | 53 | func (tr *templateRenderer) relativePath() string { 54 | templatePath := filepath.Join(tr.root, tr.templateDir) 55 | rel, _ := filepath.Rel(templatePath, tr.path) 56 | return rel 57 | } 58 | 59 | func (tr *templateRenderer) relativeDir() string { 60 | rel := tr.relativePath() 61 | dir, _ := filepath.Split(rel) 62 | return dir 63 | } 64 | 65 | func (tr *templateRenderer) readSource() (string, error) { 66 | b := bytes.NewBuffer(nil) 67 | fh, err := tr.fs.Open(tr.path) 68 | if err != nil { 69 | return b.String(), err 70 | } 71 | defer fh.Close() 72 | _, err = io.Copy(b, fh) 73 | return b.String(), err 74 | } 75 | 76 | func (tr *templateRenderer) outputWriter() (io.WriteCloser, error) { 77 | rel := tr.relativeDir() 78 | outputDir := filepath.Join(tr.outputBase, rel) 79 | err := tr.fs.MkdirAll(outputDir, os.ModePerm) 80 | if err != nil { 81 | return nil, err 82 | } 83 | fh, err := tr.fs.Create(filepath.Join(outputDir, tr.fileName())) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return fh, nil 88 | } 89 | 90 | func (tr *templateRenderer) WriteCompiled() error { 91 | contents, err := tr.readSource() 92 | if err != nil { 93 | return err 94 | } 95 | f := jen.NewFile(tr.packageName()) 96 | f.Func().Id(tr.funcName()).Params().String().Block( 97 | jen.Return(jen.Lit(contents))) 98 | fh, err := tr.outputWriter() 99 | if err != nil { 100 | return err 101 | } 102 | defer fh.Close() 103 | return f.Render(fh) 104 | } 105 | 106 | func (tr *templateRenderer) DispatchEntry() DispatchEntry { 107 | return DispatchEntry{ 108 | FuncName: tr.funcName(), 109 | RelativeImport: tr.relativeDir(), 110 | RelativePath: tr.relativePath(), 111 | PackageRoot: tr.packageRoot, 112 | } 113 | } 114 | 115 | func NewTemplateRenderer(path, root, templateDir, outputBase, packageRoot string, fs afero.Fs) TemplateRenderer { 116 | return &templateRenderer{ 117 | path: path, 118 | root: root, 119 | templateDir: templateDir, 120 | packageRoot: packageRoot, 121 | fs: fs, 122 | outputBase: outputBase, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | 11 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/integration" 12 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/provider" 13 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 14 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 15 | ) 16 | 17 | var ( 18 | gen = kingpin.New("terraform-provider-gen", "A cli for interacting with terraform providers.") 19 | 20 | updateFixturesCmd = gen.Command("update-fixtures", "update test fixtures based on current codegen output") 21 | repositoryRoot = updateFixturesCmd.Flag("repo-root", "Path to root of repository so that the fixture generator can find paths").Required().String() 22 | 23 | generateCmd = gen.Command("generate", "code generator subcommands") 24 | pluginPath = gen.Flag("plugin-path", "Path to provider plugin binary.").Required().String() 25 | providerName = gen.Flag("providerName", "Terraform provider name. must match the value given to the 'provider' directive in a terraform config.").String() 26 | 27 | outputDir = generateCmd.Flag("output-dir", "output path").String() 28 | overlayBasePath = generateCmd.Flag("overlay-dir", "Path to search for files to overlay instead of generated code. Nesting mirrors output tree.").String() 29 | cfgPath = generateCmd.Flag("cfg-path", "path to schema generation config yaml").String() 30 | 31 | bootStrapCmd = generateCmd.Command("bootstrap", "bootstrap a new provider") 32 | generateTypesCmd = generateCmd.Command("types", "Use Provider.GetSchema() to generate crossplane types.") 33 | generateRuntimeCmd = generateCmd.Command("runtime", "Generate terraform-runtime methods for generated crossplane types.") 34 | 35 | analyzeCmd = gen.Command("analyze", "perform analysis on a provider's schemas") 36 | nestingCmd = analyzeCmd.Command("nesting", "report on the different nesting paths and modes observed in a provider") 37 | nestingCmdStyle = nestingCmd.Flag("report-style", "Choose between summary (organized by nesting type and min/max), or dump (showing all nested values for all resources)").Default("dump").String() 38 | flatCmd = analyzeCmd.Command("flat", "Find resources that do not use nesting at all") 39 | typesIndexCmd = analyzeCmd.Command("cty-index", "Build an index showing which resources use which cty types") 40 | excludeTypesList = typesIndexCmd.Flag("exclude-types", "comma separated list of types to ignore (mutually exclusive with include-types)").String() 41 | includeTypesList = typesIndexCmd.Flag("include-types", "comma separated list of types to include (mutually exclusive with ignore-types)").String() 42 | listTypes = typesIndexCmd.Flag("list-types", "Only list the types, leave out the breakdown of where they can be found").Bool() 43 | ) 44 | 45 | func main() { 46 | gen.FatalIfError(run(), "Error while executing hiveworld command") 47 | } 48 | 49 | func run() error { 50 | cmd := kingpin.MustParse(gen.Parse(os.Args[1:])) 51 | switch cmd { 52 | case bootStrapCmd.FullCommand(): 53 | cfg, err := provider.ConfigFromFile(*cfgPath) 54 | if err != nil { 55 | return err 56 | } 57 | tg := template.NewCompiledTemplateGetter() 58 | p, err := client.NewGRPCProvider(cfg.Name, *pluginPath) 59 | if err != nil { 60 | return err 61 | } 62 | schema := p.GetSchema() 63 | bs := provider.NewBootstrapper(cfg, tg, schema) 64 | return bs.Bootstrap() 65 | case updateFixturesCmd.FullCommand(): 66 | opts := []integration.TestConfigOption{ 67 | integration.WithPluginPath(*pluginPath), 68 | integration.WithProvidername(*providerName), 69 | } 70 | if repositoryRoot != nil { 71 | opts = append(opts, integration.WithRepoRoot(*repositoryRoot)) 72 | } 73 | itc := integration.NewIntegrationTestConfig(opts...) 74 | err := integration.UpdateAllFixtures(itc) 75 | if err != nil { 76 | return err 77 | } 78 | case generateTypesCmd.FullCommand(), generateRuntimeCmd.FullCommand(): 79 | cfg, err := provider.ConfigFromFile(*cfgPath) 80 | if err != nil { 81 | return err 82 | } 83 | if len(cfg.ExcludeResources) > 0 { 84 | fmt.Println("Excluding the following resources from codegen:") 85 | for _, p := range cfg.ExcludeResources { 86 | fmt.Println(p) 87 | } 88 | } 89 | 90 | tg := template.NewCompiledTemplateGetter() 91 | p, err := client.NewGRPCProvider(cfg.Name, *pluginPath) 92 | if err != nil { 93 | return err 94 | } 95 | st := provider.NewSchemaTranslator(cfg, *outputDir, *overlayBasePath, p.GetSchema(), tg) 96 | 97 | switch cmd { 98 | case generateTypesCmd.FullCommand(): 99 | return st.WriteGeneratedTypes() 100 | case generateRuntimeCmd.FullCommand(): 101 | return st.WriteGeneratedRuntime() 102 | } 103 | case nestingCmd.FullCommand(): 104 | p, err := client.NewGRPCProvider(*providerName, *pluginPath) 105 | if err != nil { 106 | return err 107 | } 108 | unmm := make(integration.UniqueNestingModeMap) 109 | for name, s := range p.GetSchema().ResourceTypes { 110 | integration.VisitAllBlocks(unmm.Visitor, name, *s.Block) 111 | } 112 | switch *nestingCmdStyle { 113 | case "dump": 114 | inverted := make(map[string]integration.UniqueNestingMode) 115 | keys := make([]string, 0) 116 | for mode, paths := range unmm { 117 | for _, p := range paths { 118 | inverted[p] = mode 119 | keys = append(keys, p) 120 | } 121 | } 122 | sort.Strings(keys) 123 | for _, k := range keys { 124 | b := inverted[k] 125 | fmt.Printf("%s: %s (%d, %d, %t)\n", k, b.Mode, b.MinItems, b.MaxItems, b.IsRequired) 126 | } 127 | default: 128 | return fmt.Errorf("report-style=%s not recognized", nestingCmdStyle) 129 | } 130 | case flatCmd.FullCommand(): 131 | p, err := client.NewGRPCProvider(*providerName, *pluginPath) 132 | if err != nil { 133 | return err 134 | } 135 | frf := make(integration.FlatResourceFinder, 0) 136 | for name, s := range p.GetSchema().ResourceTypes { 137 | integration.VisitAllBlocks(frf.Visitor, name, *s.Block) 138 | } 139 | sort.Strings(frf) 140 | for _, r := range frf { 141 | fmt.Println(r) 142 | } 143 | case typesIndexCmd.FullCommand(): 144 | skipType, err := skipTypeFunc(*includeTypesList, *excludeTypesList) 145 | if err != nil { 146 | return err 147 | } 148 | cti := make(integration.CtyTypeIndexer) 149 | err = doBlockVisit(cti.Visitor) 150 | if err != nil { 151 | return err 152 | } 153 | for t, l := range cti { 154 | if skipType(t) { 155 | continue 156 | } 157 | fmt.Printf("%s:\n", t) 158 | if *listTypes { 159 | continue 160 | } 161 | for _, path := range l { 162 | fmt.Printf("\t%s\n", path) 163 | } 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | type filterFunc func(t string) bool 171 | 172 | func skipTypeFunc(incTypes, exclTypes string) (filterFunc, error) { 173 | if incTypes != "" && exclTypes != "" { 174 | return nil, fmt.Errorf("--include-types and --exclude-types flags are mutually exclusive") 175 | } 176 | if incTypes == "" && exclTypes == "" { 177 | return func(t string) bool { 178 | return false 179 | }, nil 180 | } 181 | 182 | if exclTypes != "" { 183 | ignoreTypes := make(map[string]bool) 184 | for _, tn := range strings.Split(exclTypes, ",") { 185 | ignoreTypes[tn] = true 186 | } 187 | return func(t string) bool { 188 | if _, ignored := ignoreTypes[t]; ignored { 189 | return true 190 | } 191 | return false 192 | }, nil 193 | } 194 | 195 | includeTypes := make(map[string]bool) 196 | for _, tn := range strings.Split(incTypes, ",") { 197 | includeTypes[tn] = true 198 | } 199 | return func(t string) bool { 200 | if _, included := includeTypes[t]; included { 201 | return false 202 | } 203 | return true 204 | }, nil 205 | } 206 | 207 | func doBlockVisit(visitor integration.Visitor) error { 208 | p, err := client.NewGRPCProvider(*providerName, *pluginPath) 209 | if err != nil { 210 | return err 211 | } 212 | for name, s := range p.GetSchema().ResourceTypes { 213 | integration.VisitAllBlocks(visitor, name, *s.Block) 214 | } 215 | return nil 216 | } 217 | -------------------------------------------------------------------------------- /pkg/generator/errors.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "fmt" 4 | 5 | // MultiError gives client code the ability to type assert a possible 6 | // multiError into a form where the individual errors can be examined. 7 | type MultiError interface { 8 | Append(error) 9 | Errors() []error 10 | Error() string 11 | } 12 | 13 | // multiError allows Validation methods to collect multiple errors 14 | // into a single type for deeper inspection 15 | type multiError struct { 16 | errors []error 17 | heading string 18 | } 19 | 20 | // Append adds a new error to the list of validation errors encountered 21 | func (e *multiError) Append(err error) { 22 | e.errors = append(e.errors, err) 23 | } 24 | 25 | // Errors allows access to the underlying errors, 26 | // partially satisfying the MultiError interface 27 | func (e *multiError) Errors() []error { 28 | return e.errors 29 | } 30 | 31 | // Error() combines the underlying set of validation errors into a single error message 32 | func (e *multiError) Error() string { 33 | // do not print heading if there are no errors to report 34 | if len(e.errors) == 0 { 35 | return "" 36 | } 37 | full := e.heading 38 | for _, err := range e.errors { 39 | full = fmt.Sprintf("%s\n - %s", full, err.Error()) 40 | } 41 | return full 42 | } 43 | 44 | // NewMultiError handles initializing the errors slice and allows 45 | // a heading to be set. The Error() method will print this heading 46 | // before listing out the individual errors. 47 | func NewMultiError(heading string) MultiError { 48 | return &multiError{ 49 | heading: heading, 50 | errors: make([]error, 0), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/generator/errors_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func TestMultiError(t *testing.T) { 10 | heading := "testing:" 11 | me := NewMultiError(heading) 12 | if len(me.Errors()) > 0 { 13 | t.Errorf("Observed MultiError.Errors() count=%d", len(me.Errors())) 14 | } 15 | if me.Error() != "" { 16 | t.Errorf("Got output from .Error() with empty list. Note: heading should only print if there are errors.") 17 | } 18 | me.Append(errors.New("derp")) 19 | if len(me.Errors()) != 1 { 20 | t.Errorf("Observed unexpected MultiError.Errors(). expected=1, actual=%d", len(me.Errors())) 21 | } 22 | expected := `testing: 23 | - derp` 24 | if me.Error() != expected { 25 | t.Errorf("Observed unexpected MultiError.Error() ouput. Expected:\n%s\n-----\nActual:\n%s\n", expected, me.Error()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/generator/fragment.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | j "github.com/dave/jennifer/jen" 8 | ) 9 | 10 | func renderStatement(s *j.Statement) string { 11 | return s.GoString() 12 | } 13 | 14 | // Fragment is a simple type for grouping comments with arbitrary jen statements 15 | // Jen only has nice formatting for comments in the context of a File, which is a 16 | // type we are trying to avoid in order to use Jen to only render more complex fragments 17 | // on the assumption that go templates will be more readable for files. 18 | type Fragment struct { 19 | name string 20 | comments []string 21 | statement *j.Statement 22 | } 23 | 24 | // Render concats the fragment comments with each other, 25 | // and the rendered Statement, in a predictable way 26 | func (f *Fragment) Render() string { 27 | return fmt.Sprintf("%s%s", f.FormattedComments(), renderStatement(f.statement)) 28 | } 29 | 30 | // FormattedComments uses a simple rendering algo of 31 | // prepending every line in the set of all comments with '//' 32 | // and joining them together with '\n' 33 | func (f *Fragment) FormattedComments() string { 34 | if f.comments == nil || len(f.comments) == 0 { 35 | return "" 36 | } 37 | fmtd := "" 38 | for _, c := range f.comments { 39 | for _, line := range strings.Split(c, "\n") { 40 | if line == "" { 41 | fmtd = fmtd + "\n" 42 | continue 43 | } 44 | fmtd = fmt.Sprintf("%s// %s\n", fmtd, line) 45 | } 46 | } 47 | return fmtd 48 | } 49 | -------------------------------------------------------------------------------- /pkg/generator/interfaces.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | type EncodeFnGenerator interface { 4 | GenerateEncodeFn(funcPrefix, receivedType string, f Field) string 5 | } 6 | 7 | type DecodeFnGenerator interface { 8 | GenerateDecodeFn(funcPrefix, receivedType string, f Field) string 9 | } 10 | 11 | type MergeFnGenerator interface { 12 | GenerateMergeFn(funcPrefix, receivedType string, f Field, isSpec bool) string 13 | } 14 | -------------------------------------------------------------------------------- /pkg/generator/namer.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import "fmt" 4 | 5 | type ResourceNamer interface { 6 | TypeName() string 7 | TypeListName() string 8 | SpecTypeName() string 9 | StatusTypeName() string 10 | AtProviderTypeName() string 11 | ForProviderTypeName() string 12 | } 13 | 14 | type defaultNamer struct { 15 | resourceName string 16 | } 17 | 18 | func (n defaultNamer) TypeName() string { 19 | return n.resourceName 20 | } 21 | 22 | func (n defaultNamer) TypeListName() string { 23 | return fmt.Sprintf("%sList", n.TypeName()) 24 | } 25 | 26 | func (n defaultNamer) SpecTypeName() string { 27 | return fmt.Sprintf("%sSpec", n.TypeName()) 28 | } 29 | 30 | func (n defaultNamer) StatusTypeName() string { 31 | return fmt.Sprintf("%sStatus", n.TypeName()) 32 | } 33 | 34 | func (n defaultNamer) AtProviderTypeName() string { 35 | return fmt.Sprintf("%sObservation", n.TypeName()) 36 | } 37 | 38 | func (n defaultNamer) ForProviderTypeName() string { 39 | return fmt.Sprintf("%sParameters", n.TypeName()) 40 | } 41 | 42 | func NewDefaultNamer(resourceName string) ResourceNamer { 43 | return defaultNamer{resourceName: resourceName} 44 | } 45 | -------------------------------------------------------------------------------- /pkg/generator/render.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 8 | j "github.com/dave/jennifer/jen" 9 | ) 10 | 11 | const CommentBlankLine = "" // assumes comments are joined with newlines 12 | const KubebuilderObjectRoot = "+kubebuilder:object:root=true" 13 | const KubebuilderMarkStatusSubresource = "+kubebuilder:subresource:status" 14 | 15 | // RenderKubebuilderResourceAnnotation renderes the kubebuilder resource tag 16 | // which indicates whether the resources is namespace- or cluster-scoped 17 | // and sets the categories field in the CRD which allow kubectl to select 18 | // sets of resources based on matching category tags. 19 | func RenderKubebuilderResourceAnnotation(mr *ManagedResource) string { 20 | catCSV := mr.CategoryCSV() 21 | if catCSV == "" { 22 | return "+kubebuilder:resource:scope=Cluster" 23 | } 24 | return fmt.Sprintf(" +kubebuilder:resource:scope=Cluster,categories={%s}", catCSV) 25 | } 26 | 27 | func ResourceTypeFragment(mr *ManagedResource) *Fragment { 28 | namer := mr.Namer() 29 | mrStruct := j.Type().Id(namer.TypeName()).Struct( 30 | j.Qual("metav1", "TypeMeta").Tag(map[string]string{"json": ",inline"}), 31 | j.Qual("metav1", "ObjectMeta").Tag(map[string]string{"json": "metadata,omitempty"}), 32 | j.Line(), 33 | j.Id("Spec").Qual("", namer.SpecTypeName()).Tag(map[string]string{"json": "spec"}), 34 | j.Id("Status").Qual("", namer.StatusTypeName()).Tag(map[string]string{"json": "status,omitempty"}), 35 | ) 36 | 37 | comments := []string{ 38 | KubebuilderObjectRoot, 39 | CommentBlankLine, 40 | // TODO: check if there is a reasonable description field we can use for this comment 41 | fmt.Sprintf("%s is a managed resource representing a resource mirrored in the cloud", namer.TypeName()), 42 | // TODO: handle printcolumn lines 43 | // we always mark ou resources 44 | KubebuilderMarkStatusSubresource, 45 | RenderKubebuilderResourceAnnotation(mr), 46 | } 47 | 48 | return &Fragment{ 49 | comments: comments, 50 | statement: mrStruct, 51 | } 52 | } 53 | 54 | func TypeListFragment(mr *ManagedResource) *Fragment { 55 | namer := mr.Namer() 56 | stmt := j.Type().Id(namer.TypeListName()).Struct( 57 | j.Qual("metav1", "TypeMeta").Tag(map[string]string{"json": ",inline"}), 58 | j.Qual("metav1", "ListMeta").Tag(map[string]string{"json": "metadata,omitempty"}), 59 | j.Id("Items").Index().Qual("", namer.TypeName()).Tag(map[string]string{"json": "items"}), 60 | ) 61 | comments := []string{ 62 | KubebuilderObjectRoot, 63 | CommentBlankLine, 64 | fmt.Sprintf("%s contains a list of %s", namer.TypeName(), namer.TypeListName()), 65 | } 66 | return &Fragment{ 67 | statement: stmt, 68 | comments: comments, 69 | } 70 | } 71 | 72 | func SpecFragment(mr *ManagedResource) *Fragment { 73 | namer := mr.Namer() 74 | stmt := j.Type().Id(namer.SpecTypeName()).Struct( 75 | j.Qual("xpv1", "ResourceSpec").Tag(map[string]string{"json": ",inline"}), 76 | j.Id("ForProvider").Qual("", namer.ForProviderTypeName()).Tag(map[string]string{"json": "forProvider"}), 77 | ) 78 | comment := fmt.Sprintf("A %s defines the desired state of a %s", namer.SpecTypeName(), namer.TypeName()) 79 | return &Fragment{ 80 | name: namer.SpecTypeName(), 81 | statement: stmt, 82 | comments: []string{comment}, 83 | } 84 | } 85 | 86 | func ForProviderFragments(mr *ManagedResource) []*Fragment { 87 | namer := mr.Namer() 88 | if mr.Parameters.StructField.TypeName != namer.ForProviderTypeName() { 89 | mr.Parameters.StructField.TypeName = namer.ForProviderTypeName() 90 | } 91 | frags := FieldFragments(mr.Parameters) 92 | // frags[0] is the outermost element, aka ForProvider 93 | frags[0].comments = []string{ 94 | fmt.Sprintf("A %s defines the desired state of a %s", namer.ForProviderTypeName(), namer.TypeName()), 95 | } 96 | return frags 97 | } 98 | 99 | func StatusFragment(mr *ManagedResource) *Fragment { 100 | namer := mr.Namer() 101 | stmt := j.Type().Id(namer.StatusTypeName()).Struct( 102 | j.Qual("xpv1", "ResourceStatus").Tag(map[string]string{"json": ",inline"}), 103 | j.Id("AtProvider").Qual("", namer.AtProviderTypeName()).Tag(map[string]string{"json": "atProvider"}), 104 | ) 105 | comment := fmt.Sprintf("A %s defines the observed state of a %s", namer.StatusTypeName(), namer.TypeName()) 106 | return &Fragment{ 107 | name: namer.StatusTypeName(), 108 | statement: stmt, 109 | comments: []string{comment}, 110 | } 111 | } 112 | 113 | func AtProviderFragments(mr *ManagedResource) []*Fragment { 114 | namer := mr.Namer() 115 | if mr.Observation.StructField.TypeName != namer.AtProviderTypeName() { 116 | mr.Observation.StructField.TypeName = namer.AtProviderTypeName() 117 | } 118 | frags := FieldFragments(mr.Observation) 119 | // frags[0] is the outermost element, aka AtProvider 120 | frags[0].comments = []string{ 121 | fmt.Sprintf("A %s records the observed state of a %s", namer.AtProviderTypeName(), namer.TypeName()), 122 | } 123 | return frags 124 | } 125 | 126 | func FieldFragments(f Field) []*Fragment { 127 | attributes := make([]j.Code, 0) 128 | nested := make([]*Fragment, 0) 129 | for _, a := range f.Fields { 130 | attrStatement := AttributeStatement(a, f) 131 | if attrStatement == nil { 132 | continue 133 | } 134 | attributes = append(attributes, attrStatement) 135 | if a.Type == FieldTypeStruct { 136 | for _, frag := range FieldFragments(a) { 137 | nested = append(nested, frag) 138 | } 139 | } 140 | } 141 | // append the nested fields onto the tail so that the results are 142 | // in recursive-descent order. 143 | return append([]*Fragment{{ 144 | name: f.Name, 145 | statement: j.Type().Id(f.StructField.TypeName).Struct(attributes...), 146 | }}, nested...) 147 | } 148 | 149 | func AttributeStatement(f, parent Field) *j.Statement { 150 | id := j.Id(f.Name) 151 | if f.IsSlice { 152 | id = id.Index() 153 | } 154 | switch f.Type { 155 | case FieldTypeAttribute: 156 | id = TypeStatement(f, id) 157 | if id == nil { 158 | fmt.Printf("skipping: name=%s\n", f.Name) 159 | return nil 160 | } 161 | case FieldTypeStruct: 162 | // TODO: since you can have an embedded struct, we need to allow 163 | // the name to be excluded, and since we can have relative packages 164 | // package path can be empty, but we should always have a TypeName 165 | path := f.StructField.PackagePath 166 | // this check kind of assumes that we don't refer to types that claim to 167 | // be in a different package and also nest other types. probably a safe 168 | // assumption since nesting should only be for types nested within 169 | // the same terraform package 170 | if f.StructField.PackagePath == parent.StructField.PackagePath { 171 | path = "" 172 | } 173 | id = id.Qual(path, f.StructField.TypeName) 174 | } 175 | if f.Tag != nil { 176 | if f.Tag.Json != nil { 177 | jsonTag := "" 178 | if f.Tag.Json.Name != "" { 179 | jsonTag = f.Tag.Json.Name 180 | } 181 | if f.Tag.Json.Inline { 182 | jsonTag = jsonTag + ",inline" 183 | } 184 | if f.Tag.Json.Omitempty { 185 | jsonTag = jsonTag + ",omitempty" 186 | } 187 | id.Tag(map[string]string{"json": jsonTag}) 188 | } 189 | } 190 | return id 191 | } 192 | 193 | func TypeStatement(f Field, s *j.Statement) *j.Statement { 194 | switch f.AttributeField.Type { 195 | case AttributeTypeUintptr: 196 | return s.Uintptr() 197 | case AttributeTypeUint8: 198 | return s.Uint8() 199 | case AttributeTypeUint64: 200 | return s.Uint64() 201 | case AttributeTypeUint32: 202 | return s.Uint32() 203 | case AttributeTypeUint16: 204 | return s.Uint16() 205 | case AttributeTypeUint: 206 | return s.Uint() 207 | case AttributeTypeString: 208 | return s.String() 209 | case AttributeTypeRune: 210 | return s.Rune() 211 | case AttributeTypeInt8: 212 | return s.Int8() 213 | case AttributeTypeInt64: 214 | return s.Int64() 215 | case AttributeTypeInt32: 216 | return s.Int32() 217 | case AttributeTypeInt16: 218 | return s.Int16() 219 | case AttributeTypeInt: 220 | return s.Int() 221 | case AttributeTypeFloat64: 222 | return s.Float64() 223 | case AttributeTypeFloat32: 224 | return s.Float32() 225 | case AttributeTypeComplex64: 226 | return s.Complex64() 227 | case AttributeTypeComplex128: 228 | return s.Complex128() 229 | case AttributeTypeByte: 230 | return s.Byte() 231 | case AttributeTypeBool: 232 | return s.Bool() 233 | case AttributeTypeMapStringKey: 234 | switch f.AttributeField.MapValueType { 235 | case AttributeTypeBool: 236 | return s.Map(j.String()).Bool() 237 | case AttributeTypeString: 238 | return s.Map(j.String()).String() 239 | case AttributeTypeInt: 240 | return s.Map(j.String()).Int() 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | 247 | type managedResourceTypeDefRenderer struct { 248 | mr *ManagedResource 249 | tg template.TemplateGetter 250 | } 251 | 252 | func NewManagedResourceTypeDefRenderer(mr *ManagedResource, tg template.TemplateGetter) *managedResourceTypeDefRenderer { 253 | return &managedResourceTypeDefRenderer{ 254 | mr: mr, 255 | tg: tg, 256 | } 257 | } 258 | 259 | func (tdr *managedResourceTypeDefRenderer) Render() (string, error) { 260 | mr := tdr.mr 261 | if err := mr.Validate(); err != nil { 262 | return "", err 263 | } 264 | typeDefs := make([]*Fragment, 0) 265 | 266 | typeDefs = append(typeDefs, ResourceTypeFragment(mr)) 267 | typeDefs = append(typeDefs, TypeListFragment(mr)) 268 | typeDefs = append(typeDefs, SpecFragment(mr)) 269 | for _, frag := range ForProviderFragments(mr) { 270 | typeDefs = append(typeDefs, frag) 271 | } 272 | 273 | typeDefs = append(typeDefs, StatusFragment(mr)) 274 | for _, frag := range AtProviderFragments(mr) { 275 | typeDefs = append(typeDefs, frag) 276 | } 277 | 278 | tpl, err := tdr.tg.Get("pkg/generator/types.go.tmpl") 279 | if err != nil { 280 | return "", err 281 | } 282 | 283 | typeDefsString := "" 284 | for _, f := range typeDefs { 285 | typeDefsString = fmt.Sprintf("%s\n\n%s", typeDefsString, f.Render()) 286 | } 287 | 288 | buf := new(bytes.Buffer) 289 | tplParams := struct { 290 | TypeDefs string 291 | }{typeDefsString} 292 | err = tpl.Execute(buf, tplParams) 293 | if err != nil { 294 | return "", err 295 | } 296 | return buf.String(), nil 297 | } 298 | -------------------------------------------------------------------------------- /pkg/generator/render_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | -------------------------------------------------------------------------------- /pkg/generator/sort.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | // NamedFields implements the sort.Sort methodset, allowing you to call 4 | // sort.Sort(NamedFields) on a slice of []Field 5 | type NamedFields []Field 6 | 7 | func (x NamedFields) Len() int { return len(x) } 8 | func (x NamedFields) Less(i, j int) bool { return x[i].Name < x[j].Name } 9 | func (x NamedFields) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 10 | -------------------------------------------------------------------------------- /pkg/generator/types.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type FieldType int 10 | type AttributeType int 11 | 12 | const ( 13 | FieldTypeStruct FieldType = iota 14 | FieldTypeAttribute 15 | ) 16 | 17 | //go:generate go run golang.org/x/tools/cmd/stringer -type=AttributeType,FieldType -output=types_stringers.go 18 | const ( 19 | AttributeTypeUnsupported AttributeType = iota 20 | AttributeTypeUintptr 21 | AttributeTypeUint8 22 | AttributeTypeUint64 23 | AttributeTypeUint32 24 | AttributeTypeUint16 25 | AttributeTypeUint 26 | AttributeTypeString 27 | AttributeTypeRune 28 | AttributeTypeInt8 29 | AttributeTypeInt64 30 | AttributeTypeInt32 31 | AttributeTypeInt16 32 | AttributeTypeInt 33 | AttributeTypeFloat64 34 | AttributeTypeFloat32 35 | AttributeTypeComplex64 36 | AttributeTypeComplex128 37 | AttributeTypeByte 38 | AttributeTypeBool 39 | 40 | // AttributeTypeMapStringKey means that the field is a map[string] 41 | // where is defined by the value of Field.AttributeField.MapValueType 42 | AttributeTypeMapStringKey 43 | ) 44 | 45 | var InvalidMRNameEmpty error = errors.New(".Name is required") 46 | var InvalidMRPackagePathEmpty error = errors.New(".PackagePath is required") 47 | 48 | type StructTagJson struct { 49 | Name string 50 | Omitempty bool 51 | Inline bool 52 | } 53 | 54 | type StructTag struct { 55 | Json *StructTagJson 56 | } 57 | 58 | type Field struct { 59 | Name string 60 | Type FieldType 61 | Fields []Field 62 | StructField StructField 63 | AttributeField AttributeField 64 | IsSlice bool 65 | Tag *StructTag 66 | EncodeFnGenerator EncodeFnGenerator 67 | DecodeFnGenerator DecodeFnGenerator 68 | MergeFnGenerator MergeFnGenerator 69 | 70 | // struct comment "annotations" 71 | Computed bool 72 | Optional bool 73 | Required bool 74 | Sensitive bool 75 | } 76 | 77 | type StructField struct { 78 | PackagePath string 79 | TypeName string 80 | } 81 | 82 | type AttributeField struct { 83 | Type AttributeType 84 | MapValueType AttributeType 85 | } 86 | 87 | type ManagedResource struct { 88 | Name string 89 | PackagePath string 90 | Parameters Field 91 | Observation Field 92 | namer ResourceNamer 93 | CategoryTags []string 94 | } 95 | 96 | // Validate ensures that the ManagedResource can be rendered to code 97 | func (mr *ManagedResource) Validate() error { 98 | fail := NewMultiError("ManagedResource.Validate() failed:") 99 | if mr.Name == "" { 100 | fail.Append(InvalidMRNameEmpty) 101 | } 102 | if mr.PackagePath == "" { 103 | fail.Append(InvalidMRPackagePathEmpty) 104 | } 105 | 106 | if len(fail.Errors()) > 0 { 107 | return fail 108 | } 109 | return nil 110 | } 111 | 112 | // CategoryTagsCSV returns a comma separated list respresenting CategoryTags 113 | // this is used in the kubebuilder resource categories comment annotation 114 | // eg: +kubebuilder:resource:categories={crossplane,managed,aws} 115 | func (mr *ManagedResource) CategoryCSV() string { 116 | return strings.Join(mr.CategoryTags, ",") 117 | } 118 | 119 | func (mr *ManagedResource) Namer() ResourceNamer { 120 | return mr.namer 121 | } 122 | 123 | func (mr *ManagedResource) WithNamer(n ResourceNamer) *ManagedResource { 124 | mr.namer = n 125 | return mr 126 | } 127 | 128 | func NewManagedResource(name, packagePath string) *ManagedResource { 129 | return &ManagedResource{ 130 | Name: name, 131 | PackagePath: packagePath, 132 | } 133 | } 134 | 135 | func AttributeTypeDeclaration(f Field) string { 136 | switch f.AttributeField.Type { 137 | case AttributeTypeUintptr: 138 | return "uintptr" 139 | case AttributeTypeUint8: 140 | return "uint8" 141 | case AttributeTypeUint64: 142 | return "uint64" 143 | case AttributeTypeUint32: 144 | return "uint32" 145 | case AttributeTypeUint16: 146 | return "uint16" 147 | case AttributeTypeUint: 148 | return "uint" 149 | case AttributeTypeString: 150 | return "string" 151 | case AttributeTypeRune: 152 | return "rune" 153 | case AttributeTypeInt8: 154 | return "int8" 155 | case AttributeTypeInt64: 156 | return "int64" 157 | case AttributeTypeInt32: 158 | return "int32" 159 | case AttributeTypeInt16: 160 | return "int16" 161 | case AttributeTypeInt: 162 | return "int" 163 | case AttributeTypeFloat64: 164 | return "float64" 165 | case AttributeTypeFloat32: 166 | return "float32" 167 | case AttributeTypeComplex64: 168 | return "complex64" 169 | case AttributeTypeComplex128: 170 | return "complex128" 171 | case AttributeTypeByte: 172 | return "byte" 173 | case AttributeTypeBool: 174 | return "bool" 175 | } 176 | return "panic(\"unrecognized attribute type in pkg/generator/types.go:AttributeTypeDeclaration\")" 177 | } 178 | -------------------------------------------------------------------------------- /pkg/generator/types_stringers.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AttributeType,FieldType -output=types_stringers.go"; DO NOT EDIT. 2 | 3 | package generator 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[AttributeTypeUnsupported-0] 12 | _ = x[AttributeTypeUintptr-1] 13 | _ = x[AttributeTypeUint8-2] 14 | _ = x[AttributeTypeUint64-3] 15 | _ = x[AttributeTypeUint32-4] 16 | _ = x[AttributeTypeUint16-5] 17 | _ = x[AttributeTypeUint-6] 18 | _ = x[AttributeTypeString-7] 19 | _ = x[AttributeTypeRune-8] 20 | _ = x[AttributeTypeInt8-9] 21 | _ = x[AttributeTypeInt64-10] 22 | _ = x[AttributeTypeInt32-11] 23 | _ = x[AttributeTypeInt16-12] 24 | _ = x[AttributeTypeInt-13] 25 | _ = x[AttributeTypeFloat64-14] 26 | _ = x[AttributeTypeFloat32-15] 27 | _ = x[AttributeTypeComplex64-16] 28 | _ = x[AttributeTypeComplex128-17] 29 | _ = x[AttributeTypeByte-18] 30 | _ = x[AttributeTypeBool-19] 31 | _ = x[AttributeTypeMapStringKey-20] 32 | } 33 | 34 | const _AttributeType_name = "AttributeTypeUnsupportedAttributeTypeUintptrAttributeTypeUint8AttributeTypeUint64AttributeTypeUint32AttributeTypeUint16AttributeTypeUintAttributeTypeStringAttributeTypeRuneAttributeTypeInt8AttributeTypeInt64AttributeTypeInt32AttributeTypeInt16AttributeTypeIntAttributeTypeFloat64AttributeTypeFloat32AttributeTypeComplex64AttributeTypeComplex128AttributeTypeByteAttributeTypeBoolAttributeTypeMapStringKey" 35 | 36 | var _AttributeType_index = [...]uint16{0, 24, 44, 62, 81, 100, 119, 136, 155, 172, 189, 207, 225, 243, 259, 279, 299, 321, 344, 361, 378, 403} 37 | 38 | func (i AttributeType) String() string { 39 | if i < 0 || i >= AttributeType(len(_AttributeType_index)-1) { 40 | return "AttributeType(" + strconv.FormatInt(int64(i), 10) + ")" 41 | } 42 | return _AttributeType_name[_AttributeType_index[i]:_AttributeType_index[i+1]] 43 | } 44 | func _() { 45 | // An "invalid array index" compiler error signifies that the constant values have changed. 46 | // Re-run the stringer command to generate them again. 47 | var x [1]struct{} 48 | _ = x[FieldTypeStruct-0] 49 | _ = x[FieldTypeAttribute-1] 50 | } 51 | 52 | const _FieldType_name = "FieldTypeStructFieldTypeAttribute" 53 | 54 | var _FieldType_index = [...]uint8{0, 15, 33} 55 | 56 | func (i FieldType) String() string { 57 | if i < 0 || i >= FieldType(len(_FieldType_index)-1) { 58 | return "FieldType(" + strconv.FormatInt(int64(i), 10) + ")" 59 | } 60 | return _FieldType_name[_FieldType_index[i]:_FieldType_index[i+1]] 61 | } 62 | -------------------------------------------------------------------------------- /pkg/integration/fixtures.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/optimize" 6 | "io" 7 | "os" 8 | "path" 9 | 10 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 11 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/provider" 12 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/translate" 13 | "github.com/hashicorp/terraform/configs/configschema" 14 | "github.com/hashicorp/terraform/providers" 15 | "github.com/zclconf/go-cty/cty" 16 | ) 17 | 18 | const FakeResourceName string = "Test" 19 | const FakePackagePath string = "github.com/crossplane-contrib/fake" 20 | const DefaultAPIVersion string = "v1alpha1" 21 | 22 | func DefaultTestResource() *generator.ManagedResource { 23 | return generator.NewManagedResource(FakeResourceName, FakePackagePath).WithNamer(generator.NewDefaultNamer(FakeResourceName)) 24 | } 25 | 26 | func NestedFieldsWithDuplicates() generator.Field { 27 | dupeName := "duplicatorName" 28 | dupeType := "duplicator" 29 | // d1 and d2 are identical, d3 uses the same name but contains a different field 30 | // the deduplicator should eventually fold d1 and d2 into the same underlying type 31 | // and treat d3 as a separate field 32 | d1 := generator.Field{ 33 | Name: dupeName, 34 | Type: generator.FieldTypeStruct, 35 | StructField: generator.StructField{ 36 | PackagePath: FakePackagePath, 37 | TypeName: dupeType, 38 | }, 39 | Fields: []generator.Field{ 40 | { 41 | Name: "aString", 42 | Type: generator.FieldTypeAttribute, 43 | AttributeField: generator.AttributeField{Type: generator.AttributeTypeString}, 44 | Tag: &generator.StructTag{ 45 | Json: &generator.StructTagJson{ 46 | Name: "a_string", 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | d2 := generator.Field{ 53 | Name: dupeName, 54 | Type: generator.FieldTypeStruct, 55 | StructField: generator.StructField{ 56 | PackagePath: FakePackagePath, 57 | TypeName: dupeType, 58 | }, 59 | Fields: []generator.Field{ 60 | { 61 | Name: "aString", 62 | Type: generator.FieldTypeAttribute, 63 | AttributeField: generator.AttributeField{Type: generator.AttributeTypeString}, 64 | Tag: &generator.StructTag{ 65 | Json: &generator.StructTagJson{ 66 | Name: "a_string", 67 | }, 68 | }, 69 | }, 70 | }, 71 | } 72 | d3 := generator.Field{ 73 | Name: dupeName, 74 | Type: generator.FieldTypeStruct, 75 | StructField: generator.StructField{ 76 | PackagePath: FakePackagePath, 77 | TypeName: dupeType, 78 | }, 79 | Fields: []generator.Field{ 80 | { 81 | Name: "bString", 82 | Type: generator.FieldTypeAttribute, 83 | AttributeField: generator.AttributeField{Type: generator.AttributeTypeString}, 84 | Tag: &generator.StructTag{ 85 | Json: &generator.StructTagJson{ 86 | Name: "b_string", 87 | }, 88 | }, 89 | }, 90 | }, 91 | } 92 | m1 := generator.Field{ 93 | Name: "middleOneName", 94 | Type: generator.FieldTypeStruct, 95 | StructField: generator.StructField{ 96 | PackagePath: FakePackagePath, 97 | TypeName: "middleOne", 98 | }, 99 | Fields: []generator.Field{ 100 | d1, 101 | }, 102 | } 103 | m2 := generator.Field{ 104 | Name: "middleTwoName", 105 | Type: generator.FieldTypeStruct, 106 | StructField: generator.StructField{ 107 | PackagePath: FakePackagePath, 108 | TypeName: "middleTwo", 109 | }, 110 | Fields: []generator.Field{ 111 | d2, 112 | }, 113 | } 114 | m3 := generator.Field{ 115 | Name: "middleTwoName", 116 | Type: generator.FieldTypeStruct, 117 | StructField: generator.StructField{ 118 | PackagePath: FakePackagePath, 119 | TypeName: "middleTwo", 120 | }, 121 | Fields: []generator.Field{ 122 | d3, 123 | }, 124 | } 125 | return generator.Field{ 126 | // "Name" is appended to help visually differentiate field and type names 127 | Name: "outerName", 128 | Type: generator.FieldTypeStruct, 129 | StructField: generator.StructField{ 130 | PackagePath: FakePackagePath, 131 | TypeName: "outer", 132 | }, 133 | Fields: []generator.Field{ 134 | m1, 135 | m2, 136 | m3, 137 | }, 138 | } 139 | } 140 | 141 | func NestedFieldFixture(outerTypeName, nestedTypeName, deeplyNestedTypeName string) generator.Field { 142 | f := generator.Field{ 143 | // "Name" is appended to help visually differentiate field and type names 144 | Name: deeplyNestedTypeName + "Name", 145 | Type: generator.FieldTypeStruct, 146 | StructField: generator.StructField{ 147 | PackagePath: FakePackagePath, 148 | TypeName: deeplyNestedTypeName, 149 | }, 150 | Fields: []generator.Field{ 151 | { 152 | Name: "aString", 153 | Type: generator.FieldTypeAttribute, 154 | AttributeField: generator.AttributeField{Type: generator.AttributeTypeString}, 155 | Tag: &generator.StructTag{ 156 | Json: &generator.StructTagJson{ 157 | Name: "a_string", 158 | }, 159 | }, 160 | }, 161 | }, 162 | Tag: &generator.StructTag{ 163 | Json: &generator.StructTagJson{ 164 | Name: "deeper_sub_field", 165 | }, 166 | }, 167 | } 168 | nf := generator.Field{ 169 | // "Name" is appended to help visually differentiate field and type names 170 | Name: nestedTypeName + "Name", 171 | Type: generator.FieldTypeStruct, 172 | StructField: generator.StructField{ 173 | PackagePath: FakePackagePath, 174 | TypeName: nestedTypeName, 175 | }, 176 | Fields: []generator.Field{ 177 | f, 178 | }, 179 | Tag: &generator.StructTag{ 180 | Json: &generator.StructTagJson{ 181 | Name: "sub_field", 182 | }, 183 | }, 184 | } 185 | test := generator.Field{ 186 | // "Name" is appended to help visually differentiate field and type names 187 | Name: outerTypeName + "Name", 188 | Type: generator.FieldTypeStruct, 189 | StructField: generator.StructField{ 190 | PackagePath: FakePackagePath, 191 | TypeName: outerTypeName, 192 | }, 193 | Fields: []generator.Field{ 194 | nf, 195 | }, 196 | } 197 | return test 198 | } 199 | 200 | func testFixtureFlatBlock() providers.Schema { 201 | s := providers.Schema{ 202 | Block: &configschema.Block{ 203 | Attributes: make(map[string]*configschema.Attribute), 204 | BlockTypes: make(map[string]*configschema.NestedBlock), 205 | }, 206 | } 207 | s.Block.Attributes["different_resource_ref_id"] = &configschema.Attribute{ 208 | Required: false, 209 | Optional: true, 210 | Computed: false, 211 | Type: cty.String, 212 | } 213 | s.Block.Attributes["perform_optional_action"] = &configschema.Attribute{ 214 | Required: false, 215 | Optional: true, 216 | Computed: false, 217 | Type: cty.Bool, 218 | } 219 | s.Block.Attributes["labels"] = &configschema.Attribute{ 220 | Required: false, 221 | Optional: true, 222 | Computed: false, 223 | Type: cty.Map(cty.String), 224 | } 225 | s.Block.Attributes["number_list"] = &configschema.Attribute{ 226 | Required: false, 227 | Optional: true, 228 | Computed: false, 229 | Type: cty.List(cty.Number), 230 | } 231 | s.Block.Attributes["computed_owner_id"] = &configschema.Attribute{ 232 | Required: false, 233 | Optional: false, 234 | Computed: true, 235 | Type: cty.String, 236 | } 237 | s.Block.Attributes["required_name"] = &configschema.Attribute{ 238 | Required: true, 239 | Optional: false, 240 | Computed: false, 241 | Type: cty.String, 242 | } 243 | return s 244 | } 245 | 246 | type fixtureGenerator func(*IntegrationTestConfig) (string, error) 247 | 248 | var ( 249 | TestManagedResourceTypeDefRendererPath = "testdata/test-render-types-file.go" 250 | TestRenderNestedStatusPath = "testdata/test-render-nested-status.go" 251 | TestRenderNestedSpecPath = "testdata/test-render-nested-spec.go" 252 | TestRenderDuplicateFieldSpecPath = "testdata/test-render-duplicate-field-spec.go" 253 | TestSchemaToManagedResourceRender = "testdata/test-schema-to-managed-resource-render.go" 254 | TestProviderBinarySchemaS3Path = "testdata/test-provider-binary-schema-s3.go" 255 | ) 256 | 257 | var FixtureGenerators map[string]fixtureGenerator = map[string]fixtureGenerator{ 258 | TestManagedResourceTypeDefRendererPath: func(itc *IntegrationTestConfig) (string, error) { 259 | tg, err := itc.TemplateGetter() 260 | if err != nil { 261 | return "", err 262 | } 263 | mr := DefaultTestResource() 264 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 265 | result, err := renderer.Render() 266 | return result, err 267 | }, 268 | TestRenderNestedStatusPath: func(itc *IntegrationTestConfig) (string, error) { 269 | tg, err := itc.TemplateGetter() 270 | if err != nil { 271 | return "", err 272 | } 273 | mr := DefaultTestResource() 274 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 275 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 276 | // of assuming it everywhere 277 | mr.Observation.StructField.PackagePath = FakePackagePath 278 | mr.Observation.Fields = []generator.Field{NestedFieldFixture("SubObservation", "nestedField", "deeplyNestedField")} 279 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 280 | return renderer.Render() 281 | }, 282 | TestRenderNestedSpecPath: func(itc *IntegrationTestConfig) (string, error) { 283 | tg, err := itc.TemplateGetter() 284 | if err != nil { 285 | return "", err 286 | } 287 | mr := DefaultTestResource() 288 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 289 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 290 | // of assuming it everywhere 291 | mr.Parameters.StructField.PackagePath = FakePackagePath 292 | mr.Parameters.Fields = []generator.Field{NestedFieldFixture("SubParameters", "nestedField", "deeplyNestedField")} 293 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 294 | return renderer.Render() 295 | }, 296 | TestRenderDuplicateFieldSpecPath: func(itc *IntegrationTestConfig) (string, error) { 297 | tg, err := itc.TemplateGetter() 298 | if err != nil { 299 | return "", err 300 | } 301 | mr := DefaultTestResource() 302 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 303 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 304 | // of assuming it everywhere 305 | mr.Parameters.StructField.PackagePath = FakePackagePath 306 | mr.Parameters.Fields = []generator.Field{NestedFieldsWithDuplicates()} 307 | mr, err = optimize.Deduplicate(mr) 308 | if err != nil { 309 | return "", err 310 | } 311 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 312 | return renderer.Render() 313 | }, 314 | TestSchemaToManagedResourceRender: func(itc *IntegrationTestConfig) (string, error) { 315 | tg, err := itc.TemplateGetter() 316 | if err != nil { 317 | return "", err 318 | } 319 | resourceName := "TestResource" 320 | // TODO: write some package naming stuff -- maybe start with a flat package name scheme 321 | packagePath := "github.com/crossplane/provider-terraform-aws/generated/test/v1alpha1" 322 | s := testFixtureFlatBlock() 323 | mr := translate.SchemaToManagedResource(resourceName, packagePath, s) 324 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 325 | return renderer.Render() 326 | }, 327 | TestProviderBinarySchemaS3Path: func(itc *IntegrationTestConfig) (string, error) { 328 | tg, err := itc.TemplateGetter() 329 | if err != nil { 330 | return "", err 331 | } 332 | packagePath := "github.com/crossplane/provider-terraform-aws/generated/test/v1alpha1" 333 | typeName := "aws_s3_bucket" 334 | c, err := getProvider(itc) 335 | if err != nil { 336 | return "", err 337 | } 338 | providerName, err := itc.ProviderName() 339 | if err != nil { 340 | return "", err 341 | } 342 | namer := provider.NewTerraformResourceNamer(providerName, typeName, DefaultAPIVersion) 343 | bucketResource := c.GetSchema().ResourceTypes[typeName] 344 | mr := translate.SchemaToManagedResource(namer.PackageName(), packagePath, bucketResource) 345 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, tg) 346 | return renderer.Render() 347 | }, 348 | } 349 | 350 | func UpdateAllFixtures(itc *IntegrationTestConfig) error { 351 | basePath, err := itc.RepoRoot() 352 | if err != nil { 353 | return err 354 | } 355 | for fxpath, f := range FixtureGenerators { 356 | contents, err := f(itc) 357 | if err != nil { 358 | return err 359 | } 360 | p := path.Join(basePath, "pkg/integration", fxpath) 361 | fp, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 362 | if err != nil { 363 | return err 364 | } 365 | _, err = io.WriteString(fp, contents) 366 | fp.Close() 367 | fmt.Printf("Wrote fixture %s", fxpath) 368 | if err != nil { 369 | return err 370 | } 371 | } 372 | 373 | return nil 374 | } 375 | -------------------------------------------------------------------------------- /pkg/integration/optimizer_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 5 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/optimize" 6 | "sort" 7 | "testing" 8 | ) 9 | 10 | func TestDeduplicator(t *testing.T) { 11 | mr := DefaultTestResource() 12 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 13 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 14 | // of assuming it everywhere 15 | mr.Parameters.StructField.PackagePath = FakePackagePath 16 | mr.Parameters.Fields = []generator.Field{NestedFieldsWithDuplicates()} 17 | _, err := optimize.Deduplicate(mr) 18 | if err != nil { 19 | t.Errorf("error from optimize.Deduplicate: %s", err) 20 | } 21 | fm := make(map[string][]*generator.Field) 22 | optimize.UnrollFields(&mr.Parameters, fm) 23 | AssertExistsWithLength(t, fm, "duplicator", 1) 24 | AssertExistsWithLength(t, fm, "duplicator0", 1) 25 | AssertExistsWithLength(t, fm, "duplicator1", 1) 26 | } 27 | 28 | func TestDeduplicatorIdempotent(t *testing.T) { 29 | mr := DefaultTestResource() 30 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 31 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 32 | // of assuming it everywhere 33 | mr.Parameters.StructField.PackagePath = FakePackagePath 34 | mr.Parameters.Fields = []generator.Field{NestedFieldsWithDuplicates()} 35 | _, err := optimize.Deduplicate(mr) 36 | if err != nil { 37 | t.Errorf("error from optimize.Deduplicate: %s", err) 38 | } 39 | fm := make(map[string][]*generator.Field) 40 | optimize.UnrollFields(&mr.Parameters, fm) 41 | AssertExistsWithLength(t, fm, "duplicator", 1) 42 | AssertExistsWithLength(t, fm, "duplicator0", 1) 43 | AssertExistsWithLength(t, fm, "duplicator1", 1) 44 | _, err = optimize.Deduplicate(mr) 45 | if err != nil { 46 | t.Errorf("error from optimize.Deduplicate: %s", err) 47 | } 48 | fm = make(map[string][]*generator.Field) 49 | optimize.UnrollFields(&mr.Parameters, fm) 50 | AssertExistsWithLength(t, fm, "duplicator", 1) 51 | AssertExistsWithLength(t, fm, "duplicator0", 1) 52 | AssertExistsWithLength(t, fm, "duplicator1", 1) 53 | } 54 | 55 | func AssertExistsWithLength(t *testing.T, fm map[string][]*generator.Field, name string, l int) { 56 | dup, ok := fm[name] 57 | if !ok { 58 | t.Errorf("Expected there to still be a field with TypeName='%s' after de-duplication", name) 59 | } 60 | if len(dup) != l { 61 | t.Errorf("Expected %d field with TypeName='%s' after de-duplication, observed=%d", l, name, len(dup)) 62 | } 63 | } 64 | 65 | func TestUnrollFields(t *testing.T) { 66 | mr := DefaultTestResource() 67 | // TODO: wonky thing that we have to do to satisfy matching package names to exclude 68 | // the qualifier. Might want to add generator.FakePackagePath as an arg to the fixture instead 69 | // of assuming it everywhere 70 | mr.Parameters.StructField.PackagePath = FakePackagePath 71 | mr.Parameters.Fields = []generator.Field{NestedFieldsWithDuplicates()} 72 | fm := make(map[string][]*generator.Field) 73 | optimize.UnrollFields(&mr.Parameters, fm) 74 | keys := make([]string, 0) 75 | for k, _ := range fm { 76 | keys = append(keys, k) 77 | } 78 | sort.Strings(keys) 79 | expected := []string{"duplicator", "middleOne", "middleTwo", "outer"} 80 | if !compareStringSlices(keys, expected) { 81 | t.Errorf("Unexpected set of keys after unrolling. expected=%v, actual=%v", expected, keys) 82 | } 83 | if len(fm["duplicator"]) != 3 { 84 | t.Errorf("Expected 3 duplicate fields with struct name = 'duplicator', observed=%d", len(fm["duplicator"])) 85 | } 86 | } 87 | 88 | func compareStringSlices(a, b []string) bool { 89 | if len(a) != len(b) { 90 | return false 91 | } 92 | for i, _ := range a { 93 | if a[i] != b[i] { 94 | return false 95 | } 96 | } 97 | return true 98 | } 99 | -------------------------------------------------------------------------------- /pkg/integration/render_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func getFixture(path string) (string, error) { 12 | fh, err := os.Open(path) 13 | if err != nil { 14 | return "", fmt.Errorf("Error while trying to read fixture path %s: %s", path, err) 15 | } 16 | buf := new(bytes.Buffer) 17 | _, err = io.Copy(buf, fh) 18 | if err != nil { 19 | return "", fmt.Errorf("Error while trying to read fixture path %s: %s", path, err) 20 | } 21 | return buf.String(), nil 22 | } 23 | 24 | func TestManagedResourceTypeDefRenderer(t *testing.T) { 25 | if err := AssertConsistentFixture(TestManagedResourceTypeDefRendererPath); err != nil { 26 | t.Error(err) 27 | } 28 | } 29 | 30 | func TestRenderNestedStatus(t *testing.T) { 31 | if err := AssertConsistentFixture(TestRenderNestedStatusPath); err != nil { 32 | t.Error(err) 33 | } 34 | } 35 | 36 | func TestRenderNestedSpec(t *testing.T) { 37 | if err := AssertConsistentFixture(TestRenderNestedSpecPath); err != nil { 38 | t.Error(err) 39 | } 40 | } 41 | 42 | func TestProviderBinarySchemaS3(t *testing.T) { 43 | if err := AssertConsistentFixture(TestProviderBinarySchemaS3Path); err != nil { 44 | t.Error(err) 45 | } 46 | } 47 | 48 | func AssertConsistentFixture(fixturePath string) error { 49 | fr := FixtureGenerators[fixturePath] 50 | actual, err := fr(&IntegrationTestConfig{}) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | expected, err := getFixture(fixturePath) 56 | if err != nil { 57 | return err 58 | } 59 | if actual != expected { 60 | return fmt.Errorf("Unexpected output from managedResourceTypeDefRenderer.Render().\nExpected:\n ---- \n%s\n ---- \nActual:\n ---- \n%s\n ---- \n", expected, actual) 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/integration/schema_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform/providers" 10 | "github.com/zclconf/go-cty/cty" 11 | ) 12 | 13 | func TestSchemaIterate(t *testing.T) { 14 | c, err := getProvider(&IntegrationTestConfig{}) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | vpcSchema := make(map[string]providers.Schema) 19 | vpcKeys := make([]string, 0) 20 | for name, s := range c.GetSchema().ResourceTypes { 21 | if strings.Contains(name, "aws_vpc") { 22 | vpcSchema[name] = s 23 | vpcKeys = append(vpcKeys, name) 24 | } 25 | } 26 | 27 | if _, ok := vpcSchema["aws_vpc"]; !ok { 28 | t.Errorf("Could not find vpc network in list of resource schemas! keys containing vpc=%v", vpcKeys) 29 | } 30 | } 31 | 32 | func TestNetworkACL(t *testing.T) { 33 | c, err := getProvider(&IntegrationTestConfig{}) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | rt := c.GetSchema().ResourceTypes 38 | attrs := rt["aws_network_acl"].Block.Attributes 39 | var ingressType cty.Type = attrs["ingress"].Type 40 | if !ingressType.IsSetType() { 41 | t.Errorf("Expected aws_network_acl.ingress to be a set of objects") 42 | } 43 | var ingElemsType cty.Type = ingressType.ElementType() 44 | if !ingElemsType.IsObjectType() { 45 | t.Errorf("Expected aws_network_acl.ingress to be a set of objects") 46 | } 47 | attrTypes := ingElemsType.AttributeTypes() 48 | for key, val := range attrTypes { 49 | t.Logf("%s=%v", key, val.FriendlyName()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-provider-binary-schema-s3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // S3Bucket is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type S3Bucket struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec S3BucketSpec `json:"spec"` 35 | Status S3BucketStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // S3Bucket contains a list of S3BucketList 41 | type S3BucketList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []S3Bucket `json:"items"` 45 | } 46 | 47 | // A S3BucketSpec defines the desired state of a S3Bucket 48 | type S3BucketSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider S3BucketParameters `json:"forProvider"` 51 | } 52 | 53 | // A S3BucketParameters defines the desired state of a S3Bucket 54 | type S3BucketParameters struct { 55 | Bucket string `json:"bucket"` 56 | BucketPrefix string `json:"bucket_prefix"` 57 | HostedZoneId string `json:"hosted_zone_id"` 58 | Policy string `json:"policy"` 59 | WebsiteEndpoint string `json:"website_endpoint"` 60 | AccelerationStatus string `json:"acceleration_status"` 61 | ForceDestroy bool `json:"force_destroy"` 62 | WebsiteDomain string `json:"website_domain"` 63 | Acl string `json:"acl"` 64 | Tags map[string]string `json:"tags,omitempty"` 65 | RequestPayer string `json:"request_payer"` 66 | Arn string `json:"arn"` 67 | ReplicationConfiguration ReplicationConfiguration `json:"replication_configuration"` 68 | CorsRule CorsRule `json:"cors_rule"` 69 | Logging Logging `json:"logging"` 70 | ObjectLockConfiguration ObjectLockConfiguration `json:"object_lock_configuration"` 71 | Versioning Versioning `json:"versioning"` 72 | Website Website `json:"website"` 73 | Grant Grant `json:"grant"` 74 | LifecycleRule LifecycleRule `json:"lifecycle_rule"` 75 | ServerSideEncryptionConfiguration ServerSideEncryptionConfiguration `json:"server_side_encryption_configuration"` 76 | } 77 | 78 | type ReplicationConfiguration struct { 79 | Role string `json:"role"` 80 | Rules []Rules `json:"rules"` 81 | } 82 | 83 | type Rules struct { 84 | Id string `json:"id"` 85 | Prefix string `json:"prefix"` 86 | Priority int64 `json:"priority"` 87 | Status string `json:"status"` 88 | Destination Destination `json:"destination"` 89 | Filter Filter `json:"filter"` 90 | SourceSelectionCriteria SourceSelectionCriteria `json:"source_selection_criteria"` 91 | } 92 | 93 | type Destination struct { 94 | AccountId string `json:"account_id"` 95 | Bucket string `json:"bucket"` 96 | ReplicaKmsKeyId string `json:"replica_kms_key_id"` 97 | StorageClass string `json:"storage_class"` 98 | AccessControlTranslation AccessControlTranslation `json:"access_control_translation"` 99 | } 100 | 101 | type AccessControlTranslation struct { 102 | Owner string `json:"owner"` 103 | } 104 | 105 | type Filter struct { 106 | Prefix string `json:"prefix"` 107 | Tags map[string]string `json:"tags,omitempty"` 108 | } 109 | 110 | type SourceSelectionCriteria struct { 111 | SseKmsEncryptedObjects SseKmsEncryptedObjects `json:"sse_kms_encrypted_objects"` 112 | } 113 | 114 | type SseKmsEncryptedObjects struct { 115 | Enabled bool `json:"enabled"` 116 | } 117 | 118 | type CorsRule struct { 119 | MaxAgeSeconds int64 `json:"max_age_seconds"` 120 | AllowedHeaders []string `json:"allowed_headers,omitempty"` 121 | AllowedMethods []string `json:"allowed_methods,omitempty"` 122 | AllowedOrigins []string `json:"allowed_origins,omitempty"` 123 | ExposeHeaders []string `json:"expose_headers,omitempty"` 124 | } 125 | 126 | type Logging struct { 127 | TargetBucket string `json:"target_bucket"` 128 | TargetPrefix string `json:"target_prefix"` 129 | } 130 | 131 | type ObjectLockConfiguration struct { 132 | ObjectLockEnabled string `json:"object_lock_enabled"` 133 | Rule Rule `json:"rule"` 134 | } 135 | 136 | type Rule struct { 137 | DefaultRetention DefaultRetention `json:"default_retention"` 138 | } 139 | 140 | type DefaultRetention struct { 141 | Years int64 `json:"years"` 142 | Days int64 `json:"days"` 143 | Mode string `json:"mode"` 144 | } 145 | 146 | type Versioning struct { 147 | Enabled bool `json:"enabled"` 148 | MfaDelete bool `json:"mfa_delete"` 149 | } 150 | 151 | type Website struct { 152 | ErrorDocument string `json:"error_document"` 153 | IndexDocument string `json:"index_document"` 154 | RedirectAllRequestsTo string `json:"redirect_all_requests_to"` 155 | RoutingRules string `json:"routing_rules"` 156 | } 157 | 158 | type Grant struct { 159 | Id string `json:"id"` 160 | Permissions []string `json:"permissions,omitempty"` 161 | Type string `json:"type"` 162 | Uri string `json:"uri"` 163 | } 164 | 165 | type LifecycleRule struct { 166 | AbortIncompleteMultipartUploadDays int64 `json:"abort_incomplete_multipart_upload_days"` 167 | Enabled bool `json:"enabled"` 168 | Id string `json:"id"` 169 | Prefix string `json:"prefix"` 170 | Tags map[string]string `json:"tags,omitempty"` 171 | NoncurrentVersionTransition NoncurrentVersionTransition `json:"noncurrent_version_transition"` 172 | Transition Transition `json:"transition"` 173 | Expiration Expiration `json:"expiration"` 174 | NoncurrentVersionExpiration NoncurrentVersionExpiration `json:"noncurrent_version_expiration"` 175 | } 176 | 177 | type NoncurrentVersionTransition struct { 178 | StorageClass string `json:"storage_class"` 179 | Days int64 `json:"days"` 180 | } 181 | 182 | type Transition struct { 183 | Date string `json:"date"` 184 | Days int64 `json:"days"` 185 | StorageClass string `json:"storage_class"` 186 | } 187 | 188 | type Expiration struct { 189 | Date string `json:"date"` 190 | Days int64 `json:"days"` 191 | ExpiredObjectDeleteMarker bool `json:"expired_object_delete_marker"` 192 | } 193 | 194 | type NoncurrentVersionExpiration struct { 195 | Days int64 `json:"days"` 196 | } 197 | 198 | type ServerSideEncryptionConfiguration struct { 199 | Rule Rule `json:"rule"` 200 | } 201 | 202 | type Rule struct { 203 | ApplyServerSideEncryptionByDefault ApplyServerSideEncryptionByDefault `json:"apply_server_side_encryption_by_default"` 204 | } 205 | 206 | type ApplyServerSideEncryptionByDefault struct { 207 | KmsMasterKeyId string `json:"kms_master_key_id"` 208 | SseAlgorithm string `json:"sse_algorithm"` 209 | } 210 | 211 | // A S3BucketStatus defines the observed state of a S3Bucket 212 | type S3BucketStatus struct { 213 | xpv1.ResourceStatus `json:",inline"` 214 | AtProvider S3BucketObservation `json:"atProvider"` 215 | } 216 | 217 | // A S3BucketObservation records the observed state of a S3Bucket 218 | type S3BucketObservation struct { 219 | Region string `json:"region"` 220 | BucketRegionalDomainName string `json:"bucket_regional_domain_name"` 221 | BucketDomainName string `json:"bucket_domain_name"` 222 | } 223 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-render-duplicate-field-spec.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // Test is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type Test struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec TestSpec `json:"spec"` 35 | Status TestStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // Test contains a list of TestList 41 | type TestList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []Test `json:"items"` 45 | } 46 | 47 | // A TestSpec defines the desired state of a Test 48 | type TestSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider TestParameters `json:"forProvider"` 51 | } 52 | 53 | // A TestParameters defines the desired state of a Test 54 | type TestParameters struct { 55 | outerName outer 56 | } 57 | 58 | type outer struct { 59 | middleOneName middleOne 60 | middleTwoName middleTwo 61 | middleTwoName middleTwo0 62 | } 63 | 64 | type middleOne struct { 65 | duplicatorName duplicator 66 | } 67 | 68 | type duplicator struct { 69 | aString string `json:"a_string"` 70 | } 71 | 72 | type middleTwo struct { 73 | duplicatorName duplicator0 74 | } 75 | 76 | type duplicator0 struct { 77 | aString string `json:"a_string"` 78 | } 79 | 80 | type middleTwo0 struct { 81 | duplicatorName duplicator1 82 | } 83 | 84 | type duplicator1 struct { 85 | bString string `json:"b_string"` 86 | } 87 | 88 | // A TestStatus defines the observed state of a Test 89 | type TestStatus struct { 90 | xpv1.ResourceStatus `json:",inline"` 91 | AtProvider TestObservation `json:"atProvider"` 92 | } 93 | 94 | // A TestObservation records the observed state of a Test 95 | type TestObservation struct{} 96 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-render-nested-spec.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // Test is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type Test struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec TestSpec `json:"spec"` 35 | Status TestStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // Test contains a list of TestList 41 | type TestList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []Test `json:"items"` 45 | } 46 | 47 | // A TestSpec defines the desired state of a Test 48 | type TestSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider TestParameters `json:"forProvider"` 51 | } 52 | 53 | // A TestParameters defines the desired state of a Test 54 | type TestParameters struct { 55 | SubParametersName SubParameters 56 | } 57 | 58 | type SubParameters struct { 59 | nestedFieldName nestedField `json:"sub_field"` 60 | } 61 | 62 | type nestedField struct { 63 | deeplyNestedFieldName deeplyNestedField `json:"deeper_sub_field"` 64 | } 65 | 66 | type deeplyNestedField struct { 67 | aString string `json:"a_string"` 68 | } 69 | 70 | // A TestStatus defines the observed state of a Test 71 | type TestStatus struct { 72 | xpv1.ResourceStatus `json:",inline"` 73 | AtProvider TestObservation `json:"atProvider"` 74 | } 75 | 76 | // A TestObservation records the observed state of a Test 77 | type TestObservation struct{} 78 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-render-nested-status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // Test is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type Test struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec TestSpec `json:"spec"` 35 | Status TestStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // Test contains a list of TestList 41 | type TestList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []Test `json:"items"` 45 | } 46 | 47 | // A TestSpec defines the desired state of a Test 48 | type TestSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider TestParameters `json:"forProvider"` 51 | } 52 | 53 | // A TestParameters defines the desired state of a Test 54 | type TestParameters struct{} 55 | 56 | // A TestStatus defines the observed state of a Test 57 | type TestStatus struct { 58 | xpv1.ResourceStatus `json:",inline"` 59 | AtProvider TestObservation `json:"atProvider"` 60 | } 61 | 62 | // A TestObservation records the observed state of a Test 63 | type TestObservation struct { 64 | SubObservationName SubObservation 65 | } 66 | 67 | type SubObservation struct { 68 | nestedFieldName nestedField `json:"sub_field"` 69 | } 70 | 71 | type nestedField struct { 72 | deeplyNestedFieldName deeplyNestedField `json:"deeper_sub_field"` 73 | } 74 | 75 | type deeplyNestedField struct { 76 | aString string `json:"a_string"` 77 | } 78 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-render-types-file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // Test is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type Test struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec TestSpec `json:"spec"` 35 | Status TestStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // Test contains a list of TestList 41 | type TestList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []Test `json:"items"` 45 | } 46 | 47 | // A TestSpec defines the desired state of a Test 48 | type TestSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider TestParameters `json:"forProvider"` 51 | } 52 | 53 | // A TestParameters defines the desired state of a Test 54 | type TestParameters struct{} 55 | 56 | // A TestStatus defines the observed state of a Test 57 | type TestStatus struct { 58 | xpv1.ResourceStatus `json:",inline"` 59 | AtProvider TestObservation `json:"atProvider"` 60 | } 61 | 62 | // A TestObservation records the observed state of a Test 63 | type TestObservation struct{} 64 | -------------------------------------------------------------------------------- /pkg/integration/testdata/test-schema-to-managed-resource-render.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Crossplane Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | 22 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 23 | ) 24 | 25 | // +kubebuilder:object:root=true 26 | 27 | // TestResource is a managed resource representing a resource mirrored in the cloud 28 | // +kubebuilder:subresource:status 29 | // +kubebuilder:resource:scope=Cluster 30 | type TestResource struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec TestResourceSpec `json:"spec"` 35 | Status TestResourceStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // TestResource contains a list of TestResourceList 41 | type TestResourceList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []TestResource `json:"items"` 45 | } 46 | 47 | // A TestResourceSpec defines the desired state of a TestResource 48 | type TestResourceSpec struct { 49 | xpv1.ResourceSpec `json:",inline"` 50 | ForProvider TestResourceParameters `json:"forProvider"` 51 | } 52 | 53 | // A TestResourceParameters defines the desired state of a TestResource 54 | type TestResourceParameters struct { 55 | Labels map[string]string `json:"labels,omitempty"` 56 | NumberList []int64 `json:"number_list,omitempty"` 57 | RequiredName string `json:"required_name"` 58 | DifferentResourceRefId string `json:"different_resource_ref_id"` 59 | PerformOptionalAction bool `json:"perform_optional_action"` 60 | } 61 | 62 | // A TestResourceStatus defines the observed state of a TestResource 63 | type TestResourceStatus struct { 64 | xpv1.ResourceStatus `json:",inline"` 65 | AtProvider TestResourceObservation `json:"atProvider"` 66 | } 67 | 68 | // A TestResourceObservation records the observed state of a TestResource 69 | type TestResourceObservation struct { 70 | ComputedOwnerId string `json:"computed_owner_id"` 71 | } 72 | -------------------------------------------------------------------------------- /pkg/integration/testing.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | tfplugin "github.com/hashicorp/terraform/plugin" 8 | 9 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 10 | "github.com/crossplane-contrib/terraform-runtime/pkg/client" 11 | ) 12 | 13 | var ( 14 | EnvVarProviderName = "CPTF_PLUGIN_PROVIDER_NAME" 15 | EnvVarPluginPath = "CPTF_PLUGIN_PATH" 16 | ) 17 | 18 | func getEnvOrError(name string) (string, error) { 19 | val := os.Getenv(name) 20 | if val == "" { 21 | return "", fmt.Errorf("Could not retrieve value for environment variable named %s", name) 22 | } 23 | return val, nil 24 | } 25 | 26 | func getProvider(itc *IntegrationTestConfig) (*tfplugin.GRPCProvider, error) { 27 | providerName, err := itc.ProviderName() 28 | if err != nil { 29 | return nil, err 30 | } 31 | pluginPath, err := itc.PluginPath() 32 | if err != nil { 33 | return nil, err 34 | } 35 | return client.NewGRPCProvider(providerName, pluginPath) 36 | } 37 | 38 | type IntegrationTestConfig struct { 39 | providerName string 40 | pluginPath string 41 | repoRoot string 42 | } 43 | 44 | func (itc *IntegrationTestConfig) ProviderName() (string, error) { 45 | if itc.providerName != "" { 46 | return itc.providerName, nil 47 | } 48 | return getEnvOrError(EnvVarProviderName) 49 | } 50 | 51 | func (itc *IntegrationTestConfig) PluginPath() (string, error) { 52 | if itc.pluginPath != "" { 53 | return itc.pluginPath, nil 54 | } 55 | return getEnvOrError(EnvVarPluginPath) 56 | } 57 | 58 | func (itc *IntegrationTestConfig) RepoRoot() (string, error) { 59 | if itc.repoRoot != "" { 60 | return itc.repoRoot, nil 61 | } 62 | return os.Getwd() 63 | } 64 | 65 | func (itc *IntegrationTestConfig) TemplateGetter() (template.TemplateGetter, error) { 66 | p, err := itc.RepoRoot() 67 | if err != nil { 68 | return nil, err 69 | } 70 | return template.NewFSTemplateGetter(p), nil 71 | } 72 | 73 | type TestConfigOption func(*IntegrationTestConfig) 74 | 75 | func WithRepoRoot(repoRoot string) TestConfigOption { 76 | return func(itc *IntegrationTestConfig) { 77 | itc.repoRoot = repoRoot 78 | } 79 | } 80 | 81 | func WithPluginPath(pluginPath string) TestConfigOption { 82 | return func(itc *IntegrationTestConfig) { 83 | itc.pluginPath = pluginPath 84 | } 85 | } 86 | 87 | func WithProvidername(providerName string) TestConfigOption { 88 | return func(itc *IntegrationTestConfig) { 89 | itc.providerName = providerName 90 | } 91 | } 92 | 93 | func NewIntegrationTestConfig(opts ...TestConfigOption) *IntegrationTestConfig { 94 | itc := &IntegrationTestConfig{} 95 | for _, opt := range opts { 96 | opt(itc) 97 | } 98 | return itc 99 | } 100 | -------------------------------------------------------------------------------- /pkg/integration/visitor.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/terraform/configs/configschema" 8 | 9 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/translate" 10 | ) 11 | 12 | func UnrollBlocks(block *configschema.Block, indent string) { 13 | fmt.Println("Attributes") 14 | for key, attr := range block.Attributes { 15 | fmt.Printf("%s%s(type=%s, computed=%t, optional=%t, required=%t, sensitive=%t\n", indent, key, attr.Type.FriendlyName(), attr.Computed, attr.Optional, attr.Required, attr.Sensitive) 16 | } 17 | for key, b := range block.BlockTypes { 18 | var mode string 19 | switch b.Nesting { 20 | // NestingSingle indicates that only a single instance of a given 21 | // block type is permitted, with no labels, and its content should be 22 | // provided directly as an object value. 23 | case configschema.NestingSingle: 24 | mode = "NestingSingle" 25 | 26 | // NestingGroup is similar to NestingSingle in that it calls for only a 27 | // single instance of a given block type with no labels, but it additonally 28 | // guarantees that its result will never be null, even if the block is 29 | // absent, and instead the nested attributes and blocks will be treated 30 | // as absent in that case. (Any required attributes or blocks within the 31 | // nested block are not enforced unless the block is explicitly present 32 | // in the configuration, so they are all effectively optional when the 33 | // block is not present.) 34 | // 35 | // This is useful for the situation where a remote API has a feature that 36 | // is always enabled but has a group of settings related to that feature 37 | // that themselves have default values. By using NestingGroup instead of 38 | // NestingSingle in that case, generated plans will show the block as 39 | // present even when not present in configuration, thus allowing any 40 | // default values within to be displayed to the user. 41 | case configschema.NestingGroup: 42 | mode = "NestingGroup" 43 | 44 | // NestingList indicates that multiple blocks of the given type are 45 | // permitted, with no labels, and that their corresponding objects should 46 | // be provided in a list. 47 | case configschema.NestingList: 48 | mode = "NestingList" 49 | 50 | // NestingSet indicates that multiple blocks of the given type are 51 | // permitted, with no labels, and that their corresponding objects should 52 | // be provided in a set. 53 | case configschema.NestingSet: 54 | mode = "NestingSet" 55 | 56 | // NestingMap indicates that multiple blocks of the given type are 57 | // permitted, each with a single label, and that their corresponding 58 | // objects should be provided in a map whose keys are the labels. 59 | // 60 | // It's an error, therefore, to use the same label value on multiple 61 | // blocks. 62 | case configschema.NestingMap: 63 | mode = "NestingMap" 64 | default: 65 | mode = "invalid" 66 | } 67 | fmt.Printf("%sblock key=%s; mode=%s\n", indent, key, mode) 68 | UnrollBlocks(&b.Block, indent+"\t") 69 | } 70 | } 71 | 72 | type Visitor func([]string, []*configschema.NestedBlock) 73 | 74 | func VisitAllBlocks(v Visitor, name string, block configschema.Block) { 75 | nb := configschema.NestedBlock{ 76 | Block: block, 77 | Nesting: configschema.NestingSingle, 78 | } 79 | VisitBlock(v, []string{name}, []*configschema.NestedBlock{&nb}) 80 | } 81 | 82 | func VisitBlock(v Visitor, names []string, blocks []*configschema.NestedBlock) { 83 | block := blocks[len(blocks)-1] 84 | v(names, blocks) 85 | for n, b := range block.BlockTypes { 86 | VisitBlock(v, append(names, n), append(blocks, b)) 87 | } 88 | } 89 | 90 | func tabs(n int) string { 91 | t := "" 92 | for i := 0; i < n; i++ { 93 | t += "\t" 94 | } 95 | return t 96 | } 97 | 98 | func nestingModeString(nm configschema.NestingMode) string { 99 | switch nm { 100 | // NestingSingle indicates that only a single instance of a given 101 | // block type is permitted, with no labels, and its content should be 102 | // provided directly as an object value. 103 | case configschema.NestingSingle: 104 | return "NestingSingle" 105 | 106 | // NestingGroup is similar to NestingSingle in that it calls for only a 107 | // single instance of a given block type with no labels, but it additonally 108 | // guarantees that its result will never be null, even if the block is 109 | // absent, and instead the nested attributes and blocks will be treated 110 | // as absent in that case. (Any required attributes or blocks within the 111 | // nested block are not enforced unless the block is explicitly present 112 | // in the configuration, so they are all effectively optional when the 113 | // block is not present.) 114 | // 115 | // This is useful for the situation where a remote API has a feature that 116 | // is always enabled but has a group of settings related to that feature 117 | // that themselves have default values. By using NestingGroup instead of 118 | // NestingSingle in that case, generated plans will show the block as 119 | // present even when not present in configuration, thus allowing any 120 | // default values within to be displayed to the user. 121 | case configschema.NestingGroup: 122 | return "NestingGroup" 123 | 124 | // NestingList indicates that multiple blocks of the given type are 125 | // permitted, with no labels, and that their corresponding objects should 126 | // be provided in a list. 127 | case configschema.NestingList: 128 | return "NestingList" 129 | 130 | // NestingSet indicates that multiple blocks of the given type are 131 | // permitted, with no labels, and that their corresponding objects should 132 | // be provided in a set. 133 | case configschema.NestingSet: 134 | return "NestingSet" 135 | 136 | // NestingMap indicates that multiple blocks of the given type are 137 | // permitted, each with a single label, and that their corresponding 138 | // objects should be provided in a map whose keys are the labels. 139 | // 140 | // It's an error, therefore, to use the same label value on multiple 141 | // blocks. 142 | case configschema.NestingMap: 143 | return "NestingMap" 144 | default: 145 | return "invalid" 146 | } 147 | } 148 | 149 | type UniqueNestingMode struct { 150 | Mode string 151 | MinItems int 152 | MaxItems int 153 | IsRequired bool 154 | } 155 | 156 | type UniqueNestingModeMap map[UniqueNestingMode][]string 157 | 158 | func (unmm UniqueNestingModeMap) Visitor(names []string, blocks []*configschema.NestedBlock) { 159 | b := blocks[len(blocks)-1] 160 | np := namePath(names) 161 | unm := UniqueNestingMode{ 162 | Mode: nestingModeString(b.Nesting), 163 | MinItems: b.MinItems, 164 | MaxItems: b.MaxItems, 165 | IsRequired: translate.IsBlockRequired(b), 166 | } 167 | l, ok := unmm[unm] 168 | if !ok { 169 | l = make([]string, 0) 170 | } 171 | unmm[unm] = append(l, np) 172 | } 173 | 174 | func namePath(names []string) string { 175 | return strings.Join(names, ".") 176 | } 177 | 178 | func NestingModePrinter(names []string, blocks []*configschema.NestedBlock) { 179 | b := blocks[len(blocks)-1] 180 | //fmt.Printf("%s%s: %s (%d, %d)", tabs(len(blocks)), names[len(names)-1], nestingModeString(b.Nesting), b.MinItems, b.MaxItems) 181 | fmt.Printf("%s: %s (%d, %d)", namePath(names), nestingModeString(b.Nesting), b.MinItems, b.MaxItems) 182 | if b.Deprecated { 183 | fmt.Print(" (DEPRECATED)") 184 | } 185 | fmt.Print("\n") 186 | } 187 | 188 | func MultiVisitor(vs ...Visitor) Visitor { 189 | return func(names []string, blocks []*configschema.NestedBlock) { 190 | for _, v := range vs { 191 | v(names, blocks) 192 | } 193 | } 194 | } 195 | 196 | type FlatResourceFinder []string 197 | 198 | func (frf *FlatResourceFinder) Visitor(names []string, blocks []*configschema.NestedBlock) { 199 | if len(blocks) > 1 { 200 | return 201 | } 202 | b := blocks[0] 203 | if len(b.BlockTypes) == 0 { 204 | *frf = append(*frf, names[0]) 205 | } 206 | } 207 | 208 | var _ Visitor = NestingModePrinter 209 | 210 | type CtyTypeIndexer map[string][]string 211 | 212 | func (cti CtyTypeIndexer) Visitor(names []string, blocks []*configschema.NestedBlock) { 213 | b := blocks[len(blocks)-1] 214 | for name, attr := range b.Attributes { 215 | t := attr.Type.FriendlyName() 216 | _, exists := cti[t] 217 | if !exists { 218 | cti[t] = make([]string, 0) 219 | } 220 | cti[t] = append(cti[t], namePath(append(names, name))) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /pkg/integration/visitor_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package integration 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform/configs/configschema" 9 | ) 10 | 11 | func TestNestingModeAggregator(t *testing.T) { 12 | p, err := getProvider(&IntegrationTestConfig{}) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | rt := p.GetSchema().ResourceTypes 17 | name := "aws_kinesis_analytics_application" 18 | block := rt[name].Block 19 | unmm := make(UniqueNestingModeMap) 20 | VisitAllBlocks(unmm.Visitor, name, *block) 21 | if len(unmm) != 4 { 22 | t.Errorf("Expected a single entry in UniqueNestingModeMap, found =%d", len(unmm)) 23 | } 24 | for unq, name := range unmm { 25 | t.Logf("%v : %v", unq, name) 26 | } 27 | } 28 | 29 | func TestProbe(t *testing.T) { 30 | name := "aws_appmesh_route" 31 | p, err := getProvider(&IntegrationTestConfig{}) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | rt := p.GetSchema().ResourceTypes 36 | block := rt[name].Block 37 | if len(block.Attributes) == 0 { 38 | t.Log("huh") 39 | } 40 | } 41 | 42 | type probe struct { 43 | resourceType string 44 | foundIt bool 45 | } 46 | 47 | func (p *probe) Visitor(names []string, blocks []*configschema.NestedBlock) { 48 | b := blocks[0] 49 | if b.Nesting == configschema.NestingSingle { 50 | if names[0] == p.resourceType { 51 | p.foundIt = true 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/optimize/deduplicator.go: -------------------------------------------------------------------------------- 1 | package optimize 2 | 3 | import ( 4 | "fmt" 5 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 6 | ) 7 | 8 | func Deduplicate(mr *generator.ManagedResource) (*generator.ManagedResource, error) { 9 | fm := make(map[string][]*generator.Field) 10 | UnrollFields(&mr.Parameters, fm) 11 | UnrollFields(&mr.Observation, fm) 12 | for k, f := range fm { 13 | if k == "" { 14 | continue 15 | } 16 | if len(f) > 1 { 17 | for i, _ := range f[1:] { 18 | o := i + 1 19 | collision := f[o] 20 | collision.StructField.TypeName = fmt.Sprintf("%s%d", collision.StructField.TypeName, i) 21 | } 22 | } 23 | } 24 | return mr, nil 25 | } 26 | 27 | // Build a map of Fields, keyed by Field.StructField.TypeName 28 | // this is used to by the Deduplicate optimizer find sets of duplicately named fields. 29 | // It is exported as a public function so that it can be used in the integration package to help verify 30 | // correct behavior of Deduplicate. 31 | func UnrollFields(fld *generator.Field, fm map[string][]*generator.Field) { 32 | if fld.StructField.TypeName != "" { 33 | fs, ok := fm[fld.StructField.TypeName] 34 | if !ok { 35 | fs = make([]*generator.Field, 0) 36 | } 37 | fs = append(fs, fld) 38 | fm[fld.StructField.TypeName] = fs 39 | } 40 | for i, _ := range fld.Fields { 41 | f := &fld.Fields[i] 42 | if f.Type == generator.FieldTypeStruct { 43 | UnrollFields(f, fm) 44 | } 45 | } 46 | } 47 | 48 | var _ Optimizer = Deduplicate 49 | -------------------------------------------------------------------------------- /pkg/optimize/optimizer.go: -------------------------------------------------------------------------------- 1 | package optimize 2 | 3 | import "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 4 | 5 | type Optimizer func(*generator.ManagedResource) (*generator.ManagedResource, error) 6 | 7 | func NewOptimizerChain(optimizers ...Optimizer) Optimizer { 8 | return func(mr *generator.ManagedResource) (*generator.ManagedResource, error) { 9 | var err error 10 | for _, o := range optimizers { 11 | mr, err = o(mr) 12 | if err != nil { 13 | return nil, err 14 | } 15 | } 16 | return mr, err 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/provider/bootstrap.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path" 8 | 9 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 10 | "github.com/hashicorp/terraform/providers" 11 | ) 12 | 13 | const ( 14 | PROVIDER_MAINGO_PATH = "provider/cmd/provider/main.go.tpl" 15 | 16 | PROVIDER_INDEX_PATH = "provider/generated/index.go.tpl" 17 | PROVIDERCONFIG_INIT_PATH = "provider/generated/index_provider.go.tpl" 18 | RESOURCE_IMPLEMENTATIONS_PATH = "provider/generated/index_resources.go.tpl" 19 | 20 | PROVIDERCONFIG_DOC_PATH = "provider/generated/provider/v1alpha1/doc.go.tpl" 21 | PROVIDERCONFIG_TYPES_PATH = "provider/generated/provider/v1alpha1/types.go.tpl" 22 | PROVIDERCONFIG_INDEX_PATH = "provider/generated/provider/v1alpha1/index.go.tpl" 23 | ) 24 | 25 | type Bootstrapper struct { 26 | cfg Config 27 | tg template.TemplateGetter 28 | schema providers.GetSchemaResponse 29 | } 30 | 31 | func (bs *Bootstrapper) Bootstrap() error { 32 | if err := bs.WriteMainGo(); err != nil { 33 | return err 34 | } 35 | if err := bs.WriteIndexGo(); err != nil { 36 | return err 37 | } 38 | if err := bs.WriteProviderDoc(); err != nil { 39 | return err 40 | } 41 | if err := bs.WriteProviderTypes(); err != nil { 42 | return err 43 | } 44 | if err := bs.WriteProviderIndex(); err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func (bs *Bootstrapper) WriteMainGo() error { 51 | path := path.Join(bs.cfg.BasePath, "cmd", "provider", "main.go") 52 | return bs.writeExecutedConfigTemplate(PROVIDER_MAINGO_PATH, path) 53 | } 54 | 55 | func (bs *Bootstrapper) WriteIndexGo() error { 56 | path := path.Join(bs.cfg.BasePath, "generated", "index.go") 57 | return bs.writeExecutedConfigTemplate(PROVIDER_INDEX_PATH, path) 58 | } 59 | 60 | func (bs *Bootstrapper) WriteProviderInitGo() error { 61 | path := path.Join(bs.cfg.BasePath, "generated", "index_provider.go") 62 | return bs.writeExecutedConfigTemplate(PROVIDERCONFIG_INIT_PATH, path) 63 | } 64 | 65 | func (bs *Bootstrapper) WriteProviderDoc() error { 66 | path := path.Join(bs.cfg.BasePath, "generated", "provider", bs.cfg.ProviderConfigVersion, "doc.go") 67 | return bs.writeExecutedConfigTemplate(PROVIDERCONFIG_DOC_PATH, path) 68 | } 69 | 70 | func (bs *Bootstrapper) WriteProviderTypes() error { 71 | path := path.Join(bs.cfg.BasePath, "generated", "provider", bs.cfg.ProviderConfigVersion, "types.go") 72 | return bs.writeExecutedConfigTemplate(PROVIDERCONFIG_TYPES_PATH, path) 73 | } 74 | 75 | func (bs *Bootstrapper) WriteProviderIndex() error { 76 | path := path.Join(bs.cfg.BasePath, "generated", "provider", bs.cfg.ProviderConfigVersion, "index.go") 77 | return bs.writeExecutedConfigTemplate(PROVIDERCONFIG_INDEX_PATH, path) 78 | } 79 | 80 | func (bs *Bootstrapper) writeExecutedConfigTemplate(tplPath, outPath string) error { 81 | dir := path.Dir(outPath) 82 | err := os.MkdirAll(dir, 0700) 83 | if err != nil { 84 | return err 85 | } 86 | tpl, err := bs.tg.Get(tplPath) 87 | if err != nil { 88 | return err 89 | } 90 | buf := new(bytes.Buffer) 91 | err = tpl.Execute(buf, bs.cfg) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | fh, err := os.OpenFile(outPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 97 | defer fh.Close() 98 | if err != nil { 99 | return err 100 | } 101 | _, err = io.Copy(fh, buf) 102 | return err 103 | } 104 | 105 | func NewBootstrapper(cfg Config, tg template.TemplateGetter, schema providers.GetSchemaResponse) *Bootstrapper { 106 | return &Bootstrapper{ 107 | cfg: cfg, 108 | tg: tg, 109 | schema: schema, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/provider/config.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | type Config struct { 12 | ProviderVersion string `json:"provider-version"` 13 | Name string `json:"name"` 14 | BasePath string `json:"base-path"` 15 | RootPackage string `json:"root-package"` 16 | PackagePath string `json:"package-path"` 17 | BaseCRDVersion string `json:"base-crd-version"` 18 | ProviderConfigVersion string `json:"provider-config-version"` 19 | APIGroup string `json:"api-group"` 20 | ExcludeResources []string `json:"exclude-resources"` 21 | ExcludeResourceMap map[string]bool 22 | } 23 | 24 | func (c Config) IsExcluded(resourceName string) bool { 25 | _, ok := c.ExcludeResourceMap[resourceName] 26 | return ok 27 | } 28 | 29 | func ConfigFromFile(path string) (Config, error) { 30 | c := Config{} 31 | fh, err := os.Open(path) 32 | defer fh.Close() 33 | if err != nil { 34 | return c, err 35 | } 36 | buf := new(bytes.Buffer) 37 | _, err = io.Copy(buf, fh) 38 | err = yaml.Unmarshal(buf.Bytes(), &c) 39 | if err != nil { 40 | return c, err 41 | } 42 | c.ExcludeResourceMap = make(map[string]bool) 43 | for _, er := range c.ExcludeResources { 44 | c.ExcludeResourceMap[er] = true 45 | } 46 | return c, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/provider/namer.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | ) 8 | 9 | type StringTransformer func(string) string 10 | 11 | func NewTerraformFieldRenamer() StringTransformer { 12 | return func(in string) string { 13 | return strcase.ToCamel(in) 14 | } 15 | } 16 | 17 | /* 18 | type StringTransformer func(string) string 19 | return func(in string) string 20 | ManagedResourceName() string 21 | PackageName() string 22 | APIVersion() string 23 | strippedResourceName() string 24 | TypeName() string 25 | */ 26 | 27 | type TerraformResourceNamer interface { 28 | PackageName() string 29 | ManagedResourceName() string 30 | APIVersion() string 31 | APIGroup() string 32 | KubernetesVersion() string 33 | TypeNameGroupKind() string 34 | TypeNameGroupVersionKind() string 35 | TerraformResourceName() string 36 | } 37 | 38 | type terraformResourceRenamer struct { 39 | terraformResourceName string 40 | apiVersion string 41 | providerName string 42 | } 43 | 44 | func (trr *terraformResourceRenamer) ManagedResourceName() string { 45 | return strcase.ToCamel(trr.strippedResourceName()) 46 | } 47 | func (trr *terraformResourceRenamer) ManagedResourceListName() string { 48 | return fmt.Sprintf("%sList", trr.ManagedResourceName()) 49 | } 50 | 51 | func (trr *terraformResourceRenamer) PackageName() string { 52 | return trr.strippedResourceName() 53 | } 54 | 55 | func (trr *terraformResourceRenamer) APIVersion() string { 56 | return trr.apiVersion 57 | } 58 | 59 | func (trr *terraformResourceRenamer) strippedResourceName() string { 60 | prefix := trr.providerName 61 | var offset int 62 | if prefix[len(prefix)-1:] == "_" { 63 | offset = len(prefix) 64 | } else { 65 | offset = len(prefix) + 1 66 | } 67 | return trr.terraformResourceName[offset:] 68 | } 69 | 70 | func NewTerraformResourceNamer(providerName, tfResourceName, apiVersion string) TerraformResourceNamer { 71 | return &terraformResourceRenamer{ 72 | terraformResourceName: tfResourceName, 73 | apiVersion: apiVersion, 74 | providerName: providerName, 75 | } 76 | } 77 | 78 | func (trr *terraformResourceRenamer) APIGroup() string { 79 | return fmt.Sprintf("%s.terraform-provider-%s.crossplane.io", 80 | strcase.ToKebab(trr.PackageName()), trr.providerName) 81 | } 82 | 83 | func (trr *terraformResourceRenamer) TypeName() string { 84 | return trr.ManagedResourceName() 85 | } 86 | 87 | func (trr *terraformResourceRenamer) TerraformResourceName() string { 88 | return trr.terraformResourceName 89 | } 90 | 91 | // KubernetesVersion is an alias to .APIVersion 92 | // TODO: this exists because some of the templates started using 93 | // KubernetesVersion and I haven't made up my mind as to whether I want to change it 94 | func (trr *terraformResourceRenamer) KubernetesVersion() string { 95 | return trr.APIVersion() 96 | } 97 | 98 | func (trr *terraformResourceRenamer) TypeNameGroupKind() string { 99 | return fmt.Sprintf("%sGroupKind", trr.ManagedResourceName()) 100 | } 101 | 102 | func (trr *terraformResourceRenamer) TypeNameGroupVersionKind() string { 103 | return fmt.Sprintf("%sGroupVersionKind", trr.ManagedResourceName()) 104 | } 105 | 106 | /* 107 | < package {{ .KubernetesVersion}} 108 | < name := managed.ControllerName({{ .TypeNameGroupKind }}) 109 | < resource.ManagedKind({{ .TypeNameGroupVersionKind }}), 110 | < For(&{{ .ManagedResourceName }}{}). 111 | */ 112 | -------------------------------------------------------------------------------- /pkg/provider/namer_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import "testing" 4 | 5 | func TestTerraformTypeRenamer(t *testing.T) { 6 | tfName := "aws_resource" 7 | expected := "Resource" 8 | prefix := "aws_" 9 | version := "v1alpha1" 10 | r := NewTerraformResourceNamer(prefix, tfName, version) 11 | actual := r.ManagedResourceName() 12 | if actual != expected { 13 | t.Errorf("Unexpected renaming of '%s' to '%s' using NewTerraformResourceRenamer('%s'). expected=%s", tfName, actual, prefix, expected) 14 | } 15 | prefix = "aws" 16 | r = NewTerraformResourceNamer(prefix, tfName, version) 17 | actual = r.ManagedResourceName() 18 | if actual != expected { 19 | t.Errorf("Unexpected renaming of '%s' to '%s' using NewTerraformResourceRenamer('%s'). expected=%s", tfName, actual, prefix, expected) 20 | } 21 | 22 | tfName = "aws_longer_resource_name" 23 | expected = "LongerResourceName" 24 | prefix = "aws_" 25 | r = NewTerraformResourceNamer(prefix, tfName, version) 26 | actual = r.ManagedResourceName() 27 | if actual != expected { 28 | t.Errorf("Unexpected renaming of '%s' to '%s' using NewTerraformResourceRenamer('%s'). expected=%s", tfName, actual, prefix, expected) 29 | } 30 | } 31 | 32 | func TestTerraformFieldRenamer(t *testing.T) { 33 | field_name := "meandering_long_field_name" 34 | expected := "MeanderingLongFieldName" 35 | r := NewTerraformFieldRenamer() 36 | actual := r(field_name) 37 | if actual != expected { 38 | t.Errorf("Unexpected renaming of '%s' to '%s' using NewTerraformFieldRenamer. expected=%s", field_name, actual, expected) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/provider/package.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | "syscall" 10 | 11 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 12 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 13 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/translate" 14 | "github.com/hashicorp/terraform/providers" 15 | ) 16 | 17 | func (pt *PackageTranslator) WriteTypeDefFile(mr *generator.ManagedResource) error { 18 | outputPath := pt.outputPath("types.go") 19 | fmt.Printf("Writing typedefs for %s to %s\n", pt.namer.ManagedResourceName(), outputPath) 20 | fh, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 21 | defer fh.Close() 22 | if err != nil { 23 | return err 24 | } 25 | renderer := generator.NewManagedResourceTypeDefRenderer(mr, pt.tg) 26 | rendered, err := renderer.Render() 27 | if err != nil { 28 | return err 29 | } 30 | buf := bytes.NewBufferString(rendered) 31 | _, err = io.Copy(fh, buf) 32 | return err 33 | } 34 | 35 | func (pt *PackageTranslator) WriteEncoderFile(mr *generator.ManagedResource) error { 36 | outputPath := pt.outputPath("encode.go") 37 | fmt.Printf("Writing encoder for %s to %s\n", pt.namer.ManagedResourceName(), outputPath) 38 | fh, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 39 | defer fh.Close() 40 | if err != nil { 41 | return err 42 | } 43 | generated, err := translate.GenerateEncoders(mr, pt.tg) 44 | if err != nil { 45 | return err 46 | } 47 | buf := bytes.NewBufferString(generated) 48 | _, err = io.Copy(fh, buf) 49 | return err 50 | } 51 | 52 | func (pt *PackageTranslator) WriteDecodeFile(mr *generator.ManagedResource) error { 53 | outputPath := pt.outputPath("decode.go") 54 | fmt.Printf("Writing decoder for %s to %s\n", pt.namer.ManagedResourceName(), outputPath) 55 | fh, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 56 | defer fh.Close() 57 | if err != nil { 58 | return err 59 | } 60 | generated, err := translate.GenerateDecoders(mr, pt.tg) 61 | if err != nil { 62 | return err 63 | } 64 | buf := bytes.NewBufferString(generated) 65 | _, err = io.Copy(fh, buf) 66 | return err 67 | } 68 | 69 | func (pt *PackageTranslator) WriteCompareFile(mr *generator.ManagedResource) error { 70 | outputPath := pt.outputPath("compare.go") 71 | fmt.Printf("Writing merger for %s to %s\n", pt.namer.ManagedResourceName(), outputPath) 72 | fh, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 73 | defer fh.Close() 74 | if err != nil { 75 | return err 76 | } 77 | generated, err := translate.GenerateMergers(mr, pt.tg) 78 | if err != nil { 79 | return err 80 | } 81 | buf := bytes.NewBufferString(generated) 82 | _, err = io.Copy(fh, buf) 83 | return err 84 | } 85 | 86 | func (pt *PackageTranslator) WriteConfigureFile() error { 87 | return pt.renderWithNamer("configure.go") 88 | } 89 | 90 | func (pt *PackageTranslator) WriteDocFile() error { 91 | return pt.renderWithNamer("doc.go") 92 | } 93 | 94 | func (pt *PackageTranslator) WriteIndexFile() error { 95 | return pt.renderWithNamer("index.go") 96 | } 97 | 98 | func (pt *PackageTranslator) renderWithNamer(filename string) error { 99 | overlaid, err := pt.overlaid(filename) 100 | if err != nil { 101 | return err 102 | } 103 | if overlaid { 104 | return nil 105 | } 106 | outputPath := pt.outputPath(filename) 107 | fmt.Printf("Writing %s for %s to %s\n", filename, pt.namer.ManagedResourceName(), outputPath) 108 | fh, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 109 | defer fh.Close() 110 | if err != nil { 111 | return err 112 | } 113 | ttpl, err := pt.tg.Get(pt.templatePath(filename)) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | buf := new(bytes.Buffer) 119 | err = ttpl.Execute(buf, pt.namer) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | _, err = io.Copy(fh, buf) 125 | return err 126 | } 127 | 128 | func (pt *PackageTranslator) overlaid(filename string) (bool, error) { 129 | if pt.overlayBasePath == "" { 130 | return false, nil 131 | } 132 | overlayDir := pt.resourcePath(pt.overlayBasePath) 133 | // we treat overlay files as .txt so they don't confuse the compiler 134 | ftxt := fmt.Sprintf("%s.txt", filename) 135 | overlayPath := path.Join(overlayDir, ftxt) 136 | if _, err := os.Stat(overlayPath); os.IsNotExist(err) || err == syscall.ENOTDIR { 137 | return false, nil 138 | } 139 | in, err := os.Open(overlayPath) 140 | if err != nil { 141 | return false, err 142 | } 143 | defer in.Close() 144 | outputPath := pt.outputPath(filename) 145 | out, err := os.Create(outputPath) 146 | if err != nil { 147 | return false, err 148 | } 149 | defer out.Close() 150 | _, err = io.Copy(out, in) 151 | fmt.Printf("Overlayed %s onto %s\n", overlayPath, outputPath) 152 | return true, err 153 | } 154 | 155 | func (pt *PackageTranslator) templatePath(filename string) string { 156 | return fmt.Sprintf("pkg/generator/%s.tmpl", filename) 157 | } 158 | 159 | func (pt *PackageTranslator) outputPath(filename string) string { 160 | return path.Join(pt.outputDir(), filename) 161 | } 162 | 163 | func (pt *PackageTranslator) outputDir() string { 164 | return pt.resourcePath(pt.basePath) 165 | } 166 | 167 | func (pt *PackageTranslator) resourcePath(base string) string { 168 | return path.Join(base, pt.namer.PackageName(), pt.namer.APIVersion()) 169 | } 170 | 171 | func (pt *PackageTranslator) EnsureOutputLocation() error { 172 | fmt.Printf("creating basepath=%s\n", pt.basePath) 173 | err := os.MkdirAll(pt.outputDir(), 0700) 174 | if err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | 180 | type PackageTranslator struct { 181 | namer TerraformResourceNamer 182 | resourceSchema providers.Schema 183 | cfg Config 184 | tg template.TemplateGetter 185 | basePath string 186 | overlayBasePath string 187 | } 188 | 189 | type PackageImport struct { 190 | Name string 191 | Path string 192 | } 193 | 194 | func (pt *PackageTranslator) PackageImport() PackageImport { 195 | return PackageImport{ 196 | Name: pt.namer.PackageName(), 197 | Path: path.Join(pt.cfg.PackagePath, pt.namer.PackageName(), pt.namer.APIVersion()), 198 | } 199 | } 200 | 201 | func NewPackageTranslator(s providers.Schema, namer TerraformResourceNamer, basePath, overlayBasePath string, cfg Config, tg template.TemplateGetter) *PackageTranslator { 202 | return &PackageTranslator{ 203 | namer: namer, 204 | resourceSchema: s, 205 | cfg: cfg, 206 | tg: tg, 207 | basePath: basePath, 208 | overlayBasePath: overlayBasePath, 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /pkg/provider/schema.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/optimize" 7 | "io" 8 | "os" 9 | "path" 10 | "sort" 11 | 12 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/template" 13 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/translate" 14 | "github.com/hashicorp/terraform/providers" 15 | ) 16 | 17 | type SchemaTranslatorConfiguration struct { 18 | CRDVersion string 19 | PackagePath string 20 | ProviderName string 21 | } 22 | 23 | type SchemaTranslator struct { 24 | cfg Config 25 | schema providers.GetSchemaResponse 26 | renamer StringTransformer 27 | tg template.TemplateGetter 28 | basePath string 29 | overlayBasePath string 30 | } 31 | 32 | func (st *SchemaTranslator) WriteGeneratedTypes() error { 33 | for name, s := range st.schema.ResourceTypes { 34 | if st.cfg.IsExcluded(name) { 35 | fmt.Printf("Skipping resource %s", name) 36 | continue 37 | } 38 | namer := NewTerraformResourceNamer(st.cfg.Name, name, st.cfg.BaseCRDVersion) 39 | pt := NewPackageTranslator(s, namer, st.basePath, st.overlayBasePath, st.cfg, st.tg) 40 | err := pt.EnsureOutputLocation() 41 | if err != nil { 42 | return err 43 | } 44 | mr := translate.SchemaToManagedResource(pt.namer.ManagedResourceName(), pt.cfg.PackagePath, pt.resourceSchema) 45 | mr, err = optimize.Deduplicate(mr) 46 | if err != nil { 47 | return err 48 | } 49 | err = pt.WriteTypeDefFile(mr) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = pt.WriteDocFile() 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | func (st *SchemaTranslator) WriteGeneratedRuntime() error { 63 | pis := make([]PackageImport, 0) 64 | for name, s := range st.schema.ResourceTypes { 65 | if st.cfg.IsExcluded(name) { 66 | fmt.Printf("Skipping resource %s", name) 67 | continue 68 | } 69 | namer := NewTerraformResourceNamer(st.cfg.Name, name, st.cfg.BaseCRDVersion) 70 | pt := NewPackageTranslator(s, namer, st.basePath, st.overlayBasePath, st.cfg, st.tg) 71 | err := pt.EnsureOutputLocation() 72 | if err != nil { 73 | return err 74 | } 75 | mr := translate.SchemaToManagedResource(pt.namer.ManagedResourceName(), pt.cfg.PackagePath, pt.resourceSchema) 76 | mr, err = optimize.Deduplicate(mr) 77 | if err != nil { 78 | return err 79 | } 80 | err = pt.WriteEncoderFile(mr) 81 | if err != nil { 82 | return err 83 | } 84 | err = pt.WriteDecodeFile(mr) 85 | if err != nil { 86 | return err 87 | } 88 | err = pt.WriteCompareFile(mr) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | err = pt.WriteConfigureFile() 94 | if err != nil { 95 | return err 96 | } 97 | err = pt.WriteIndexFile() 98 | if err != nil { 99 | return err 100 | } 101 | pis = append(pis, pt.PackageImport()) 102 | } 103 | return st.writeResourceImplementationIndex(pis) 104 | } 105 | 106 | func (st *SchemaTranslator) writeResourceImplementationIndex(pis []PackageImport) error { 107 | dir := path.Dir(st.basePath) 108 | err := os.MkdirAll(dir, 0700) 109 | if err != nil { 110 | return err 111 | } 112 | tpl, err := st.tg.Get(RESOURCE_IMPLEMENTATIONS_PATH) 113 | if err != nil { 114 | return err 115 | } 116 | // sort ascending by package name for stable output 117 | sort.SliceStable(pis, func(i, j int) bool { 118 | return pis[i].Name < pis[j].Name 119 | }) 120 | buf := new(bytes.Buffer) 121 | values := struct { 122 | Config 123 | PackageImports []PackageImport 124 | }{ 125 | Config: st.cfg, 126 | PackageImports: pis, 127 | } 128 | err = tpl.Execute(buf, values) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | outPath := path.Join(dir, "index_resources.go") 134 | fh, err := os.OpenFile(outPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 135 | defer fh.Close() 136 | if err != nil { 137 | return err 138 | } 139 | _, err = io.Copy(fh, buf) 140 | return err 141 | } 142 | 143 | func NewSchemaTranslator(cfg Config, basePath, overlayBasePath string, schema providers.GetSchemaResponse, tg template.TemplateGetter) *SchemaTranslator { 144 | return &SchemaTranslator{ 145 | overlayBasePath: overlayBasePath, 146 | basePath: basePath, 147 | cfg: cfg, 148 | schema: schema, 149 | tg: tg, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pkg/template/compiled.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "fmt" 5 | compiled "github.com/crossplane-contrib/terraform-provider-gen/internal/template/compiled" 6 | tmpl "text/template" 7 | ) 8 | 9 | type compiledTemplateGetter struct { 10 | } 11 | 12 | func (ctg *compiledTemplateGetter) Get(path string) (*tmpl.Template, error) { 13 | cb, ok := compiled.TemplateDispatchMap[path] 14 | if !ok { 15 | return nil, fmt.Errorf("Compiled template not found for path %s", path) 16 | } 17 | return tmpl.New(path).Parse(cb()) 18 | } 19 | 20 | func NewCompiledTemplateGetter() TemplateGetter { 21 | return &compiledTemplateGetter{} 22 | } 23 | -------------------------------------------------------------------------------- /pkg/template/fs.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path" 8 | tmpl "text/template" 9 | ) 10 | 11 | type fsTplGetter struct { 12 | basepath string 13 | } 14 | 15 | func fileAsString(fname string) (string, error) { 16 | fh, err := os.Open(fname) 17 | defer fh.Close() 18 | if err != nil { 19 | return "", err 20 | } 21 | buf := new(bytes.Buffer) 22 | _, err = io.Copy(buf, fh) 23 | if err != nil { 24 | return "", err 25 | } 26 | return buf.String(), nil 27 | } 28 | 29 | func (tg *fsTplGetter) Get(p string) (*tmpl.Template, error) { 30 | str, err := fileAsString(path.Join(tg.basepath, p)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return tmpl.New(p).Parse(str) 35 | } 36 | 37 | // NewFSTemplateGetter returns a template getter that can find templates 38 | // from paths within the root "hack" directory of the repository 39 | // it assumes its own relative path within the project structure 40 | // see tests get_tests.go to help understand how it is used. 41 | func NewFSTemplateGetter(basepath string) TemplateGetter { 42 | return &fsTplGetter{ 43 | basepath: basepath, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/template/fs_test.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestTemplateGetter(t *testing.T) { 9 | g := NewFSTemplateGetter("../..") 10 | f, err := g.Get("hack/template/pkg/template/test-template-getter.txt") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | buf := new(bytes.Buffer) 15 | err = f.Execute(buf, struct{}{}) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | actual := buf.String() 20 | expected := "see tests for TemplateGetter in pkg/template to understand why this file is here.\n" 21 | if actual != expected { 22 | t.Errorf("Expected to find fixture content:\n%s\nInstead found content\n%s\n", expected, actual) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/template/get.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | tmpl "text/template" 5 | ) 6 | 7 | // TemplateGetter encapsulates the task of getting template data from 8 | // the repository. 9 | type TemplateGetter interface { 10 | // Get retrieves a Template parsed from the file indicated by the path argument. 11 | // The path argument is relative to the hack directory at the root of this repo. 12 | Get(path string) (*tmpl.Template, error) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/translate/encoders_test.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestRenderPrimitiveType(t *testing.T) { 11 | f := generator.Field{ 12 | Name: "SomeAttribute", 13 | } 14 | bt := &backTracker{ 15 | tfName: "some_attribute_tf_name", 16 | ctyType: cty.String, 17 | } 18 | funcPrefix := "encodeResource_Spec_ForProvider" 19 | receivedType := "ForProvider" 20 | 21 | actual := bt.GenerateEncodeFn(funcPrefix, receivedType, f) 22 | expected := `func encodeResource_Spec_ForProvider_SomeAttribute(p *ForProvider, vals map[string]cty.Value) { 23 | vals["some_attribute_tf_name"] = cty.StringVal(p.SomeAttribute) 24 | }` 25 | if actual != expected { 26 | t.Errorf("Expected:\n----\n%s\n----\nActual:\n----\n%s\n---", expected, actual) 27 | } 28 | } 29 | 30 | func TestRenderPrimitiveCollectionType(t *testing.T) { 31 | f := generator.Field{ 32 | Name: "SomeAttribute", 33 | } 34 | ls := cty.List(cty.String) 35 | bt := &backTracker{ 36 | tfName: "some_attribute_tf_name", 37 | ctyType: cty.String, 38 | collectionType: &ls, 39 | } 40 | funcPrefix := "encodeResource_Spec_ForProvider" 41 | receivedType := "ForProvider" 42 | 43 | actual := bt.GenerateEncodeFn(funcPrefix, receivedType, f) 44 | expected := `func encodeResource_Spec_ForProvider_SomeAttribute(p *ForProvider, vals map[string]cty.Value) { 45 | colVals := make([]cty.Value, 0) 46 | for _, value := range p.SomeAttribute { 47 | colVals = append(colVals, cty.StringVal(value)) 48 | } 49 | vals["some_attribute_tf_name"] = cty.ListVal(colVals) 50 | }` 51 | if actual != expected { 52 | t.Errorf("Expected:\n----\n%s\n----\nActual:\n----\n%s\n---", expected, actual) 53 | } 54 | } 55 | 56 | func TestRenderContainerType(t *testing.T) { 57 | f := generator.Field{ 58 | Name: "NestedField", 59 | Fields: []generator.Field{ 60 | { 61 | Name: "AttributeOne", 62 | Type: generator.FieldTypeAttribute, 63 | EncodeFnGenerator: &backTracker{ 64 | tfName: "attribute_one_tf_name", 65 | ctyType: cty.String, 66 | }, 67 | }, 68 | { 69 | Name: "DeeperField", 70 | Type: generator.FieldTypeStruct, 71 | EncodeFnGenerator: &backTracker{ 72 | tfName: "deeper_field_tf_name", 73 | ctyType: cty.Object(map[string]cty.Type{ 74 | "deeper_attribute_one_tf_name": cty.String, 75 | }), 76 | }, 77 | Fields: []generator.Field{ 78 | { 79 | Name: "DeeperAttributeOne", 80 | Type: generator.FieldTypeAttribute, 81 | EncodeFnGenerator: &backTracker{ 82 | tfName: "deeper_attribute_one_tf_name", 83 | ctyType: cty.String, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | Type: generator.FieldTypeStruct, 90 | EncodeFnGenerator: &backTracker{ 91 | tfName: "nested_field_tf_name", 92 | ctyType: cty.Object(map[string]cty.Type{ 93 | "attribute_one_tf_name": cty.String, 94 | "deeper_attribute_one_tf_name": cty.Object(map[string]cty.Type{ 95 | "deeper_attribute_one_tf_name": cty.String, 96 | }), 97 | }), 98 | }, 99 | } 100 | funcPrefix := "encodeResource_Spec_ForProvider" 101 | receivedType := "NestedField" 102 | actual := f.EncodeFnGenerator.GenerateEncodeFn(funcPrefix, receivedType, f) 103 | expected := `func encodeResource_Spec_ForProvider_NestedField(p *NestedField, vals map[string]cty.Value) { 104 | ctyVal = make(map[string]cty.Value) 105 | encodeResource_Spec_ForProvider_NestedField_AttributeOne(p, ctyVal) 106 | encodeResource_Spec_ForProvider_NestedField_DeeperField(p.DeeperField, ctyVal) 107 | vals["nested_field_tf_name"] = cty.ObjectVal(ctyVal) 108 | } 109 | 110 | func encodeResource_Spec_ForProvider_NestedField_AttributeOne(p *NestedField, vals map[string]cty.Value) { 111 | vals["attribute_one_tf_name"] = cty.StringVal(p.AttributeOne) 112 | } 113 | 114 | func encodeResource_Spec_ForProvider_NestedField_DeeperField(p *DeeperField, vals map[string]cty.Value) { 115 | ctyVal = make(map[string]cty.Value) 116 | encodeResource_Spec_ForProvider_NestedField_DeeperField_DeeperAttributeOne(p, ctyVal) 117 | vals["deeper_field_tf_name"] = cty.ObjectVal(ctyVal) 118 | } 119 | 120 | func encodeResource_Spec_ForProvider_NestedField_DeeperField_DeeperAttributeOne(p *DeeperField, vals map[string]cty.Value) { 121 | vals["deeper_attribute_one_tf_name"] = cty.StringVal(p.DeeperAttributeOne) 122 | }` 123 | if actual != expected { 124 | t.Errorf("Expected:\n----\n%s\n----\nActual:\n----\n%s\n---", expected, actual) 125 | } 126 | } 127 | 128 | func TestRenderContainerCollectionType(t *testing.T) { 129 | lt := cty.List(cty.EmptyObject) 130 | f := generator.Field{ 131 | Name: "NestedField", 132 | Fields: []generator.Field{ 133 | { 134 | Name: "AttributeOne", 135 | Type: generator.FieldTypeAttribute, 136 | EncodeFnGenerator: &backTracker{ 137 | tfName: "attribute_one_tf_name", 138 | ctyType: cty.String, 139 | }, 140 | }, 141 | { 142 | Name: "DeeperField", 143 | Type: generator.FieldTypeStruct, 144 | EncodeFnGenerator: &backTracker{ 145 | tfName: "deeper_field_tf_name", 146 | ctyType: cty.EmptyObject, 147 | }, 148 | Fields: []generator.Field{ 149 | { 150 | Name: "DeeperAttributeOne", 151 | Type: generator.FieldTypeAttribute, 152 | EncodeFnGenerator: &backTracker{ 153 | tfName: "deeper_attribute_one_tf_name", 154 | ctyType: cty.String, 155 | }, 156 | }, 157 | }, 158 | }, 159 | }, 160 | Type: generator.FieldTypeStruct, 161 | EncodeFnGenerator: &backTracker{ 162 | tfName: "nested_field_tf_name", 163 | ctyType: cty.EmptyObject, 164 | collectionType: <, 165 | }, 166 | } 167 | funcPrefix := "encodeResource_Spec_ForProvider" 168 | receivedType := "NestedField" 169 | actual := f.EncodeFnGenerator.GenerateEncodeFn(funcPrefix, receivedType, f) 170 | expected := `func encodeResource_Spec_ForProvider_NestedField(p *NestedField, vals map[string]cty.Value) { 171 | valsForCollection = make([]cty.Value, 0) 172 | for _, v := range p.NestedField { 173 | ctyVal = make(map[string]cty.Value) 174 | encodeResource_Spec_ForProvider_NestedField_AttributeOne(v, ctyVal) 175 | encodeResource_Spec_ForProvider_NestedField_DeeperField(v.DeeperField, ctyVal) 176 | valsForCollection = append(valsForCollection, cty.ObjectVal(ctyVal)) 177 | } 178 | vals["nested_field_tf_name"] = cty.ListVal(valsForCollection) 179 | } 180 | 181 | func encodeResource_Spec_ForProvider_NestedField_AttributeOne(p *NestedField, vals map[string]cty.Value) { 182 | vals["attribute_one_tf_name"] = cty.StringVal(p.AttributeOne) 183 | } 184 | 185 | func encodeResource_Spec_ForProvider_NestedField_DeeperField(p *DeeperField, vals map[string]cty.Value) { 186 | ctyVal = make(map[string]cty.Value) 187 | encodeResource_Spec_ForProvider_NestedField_DeeperField_DeeperAttributeOne(p, ctyVal) 188 | vals["deeper_field_tf_name"] = cty.ObjectVal(ctyVal) 189 | } 190 | 191 | func encodeResource_Spec_ForProvider_NestedField_DeeperField_DeeperAttributeOne(p *DeeperField, vals map[string]cty.Value) { 192 | vals["deeper_attribute_one_tf_name"] = cty.StringVal(p.DeeperAttributeOne) 193 | }` 194 | if actual != expected { 195 | t.Errorf("Expected:\n----\n%s\n----\nActual:\n----\n%s\n---", expected, actual) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /pkg/translate/translator_test.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 7 | "github.com/hashicorp/terraform/configs/configschema" 8 | "github.com/hashicorp/terraform/providers" 9 | "github.com/iancoleman/strcase" 10 | "github.com/zclconf/go-cty/cty" 11 | ) 12 | 13 | func testFixtureOptionalStringField() (string, string, *configschema.Attribute) { 14 | return "optional_field", "OptionalField", &configschema.Attribute{ 15 | Required: false, 16 | Optional: true, 17 | Computed: false, 18 | Type: cty.String, 19 | } 20 | } 21 | 22 | func TestTypeToField(t *testing.T) { 23 | name, expectedName, attr := testFixtureOptionalStringField() 24 | f := TypeToField(name, attr.Type, "") 25 | if f.Name != expectedName { 26 | t.Errorf("Wrong value from TypeToField for Field.Name. expected=%s, actual=%s", expectedName, f.Name) 27 | } 28 | if (f.Type != generator.FieldTypeAttribute && f.AttributeField != generator.AttributeField{}) { 29 | t.Errorf("Expected TypeToField to return an Attribute field, instead saw=%s", f.Type.String()) 30 | } 31 | if f.AttributeField.Type != generator.AttributeTypeString { 32 | t.Errorf("Expected attribute field to be a string type, instead saw =%s", f.AttributeField.Type.String()) 33 | } 34 | } 35 | 36 | func testFixtureFlatBlock() providers.Schema { 37 | s := providers.Schema{ 38 | Block: &configschema.Block{ 39 | Attributes: make(map[string]*configschema.Attribute), 40 | BlockTypes: make(map[string]*configschema.NestedBlock), 41 | }, 42 | } 43 | // I think "id" should probably not be part of the schema, it is like our external-name 44 | // TODO: check how this was implemented in the prototype 45 | //s.Block.Attributes["id"] = 46 | s.Block.Attributes["different_resource_ref_id"] = &configschema.Attribute{ 47 | Required: false, 48 | Optional: true, 49 | Computed: false, 50 | Type: cty.String, 51 | } 52 | s.Block.Attributes["perform_optional_action"] = &configschema.Attribute{ 53 | Required: false, 54 | Optional: true, 55 | Computed: false, 56 | Type: cty.Bool, 57 | } 58 | s.Block.Attributes["labels"] = &configschema.Attribute{ 59 | Required: false, 60 | Optional: true, 61 | Computed: false, 62 | Type: cty.Map(cty.String), 63 | } 64 | s.Block.Attributes["number_list"] = &configschema.Attribute{ 65 | Required: false, 66 | Optional: true, 67 | Computed: false, 68 | Type: cty.List(cty.Number), 69 | } 70 | s.Block.Attributes["computed_owner_id"] = &configschema.Attribute{ 71 | Required: false, 72 | Optional: false, 73 | Computed: true, 74 | Type: cty.String, 75 | } 76 | s.Block.Attributes["required_name"] = &configschema.Attribute{ 77 | Required: true, 78 | Optional: false, 79 | Computed: false, 80 | Type: cty.String, 81 | } 82 | return s 83 | } 84 | 85 | func TestSpecStatusAttributeFields(t *testing.T) { 86 | resourceName := "test" 87 | s := testFixtureFlatBlock() 88 | namer := generator.NewDefaultNamer(strcase.ToCamel(resourceName)) 89 | fp, ap := SpecOrStatusAttributeFields(s.Block.Attributes, namer) 90 | total := len(s.Block.Attributes) 91 | expectedAP := 1 92 | expectedFP := total - expectedAP 93 | if len(fp) != expectedFP { 94 | t.Errorf("Expected %d/%d fields to be in ForProvider, saw=%d", expectedFP, total, len(fp)) 95 | } 96 | if len(ap) != expectedAP { 97 | t.Errorf("Expected %d/%d fields to be in AtProvider, saw=%d", expectedAP, total, len(ap)) 98 | } 99 | } 100 | 101 | func TestSchemaToManagedResourceRender(t *testing.T) { 102 | resourceName := "TestResource" 103 | // TODO: write some package naming stuff -- maybe start with a flat package name scheme 104 | packagePath := "github.com/crossplane/provider-terraform-aws/generated/test/v1alpha1" 105 | s := testFixtureFlatBlock() 106 | mr := SchemaToManagedResource(resourceName, packagePath, s) 107 | if mr.Name != mr.Namer().TypeName() { 108 | t.Errorf("expected ManagedResource.Name=%s, actual=%s", mr.Namer().TypeName(), mr.Name) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/translate/translators.go: -------------------------------------------------------------------------------- 1 | package translate 2 | 3 | import ( 4 | "fmt" 5 | "github.com/crossplane-contrib/terraform-provider-gen/pkg/generator" 6 | "github.com/hashicorp/terraform/configs/configschema" 7 | "github.com/hashicorp/terraform/providers" 8 | "github.com/iancoleman/strcase" 9 | "github.com/zclconf/go-cty/cty" 10 | "sort" 11 | ) 12 | 13 | type SpecOrStatusField int 14 | 15 | const ( 16 | ForProviderField SpecOrStatusField = iota 17 | AtProviderField 18 | ) 19 | 20 | type FieldBuilder struct { 21 | f *generator.Field 22 | } 23 | 24 | func isReferenceType(t cty.Type) bool { 25 | if t.IsMapType() { 26 | return true 27 | } 28 | if t.IsCollectionType() { 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | func NewFieldBuilder(name string, ctyType cty.Type) *FieldBuilder { 35 | encFnGen := NewAttributeEncodeFnGenerator(name, ctyType) 36 | decFnGen := NewAttributeDecodeFnGenerator(name, ctyType) 37 | mergeFnGen := NewAttributeMergeFnGenerator(name, ctyType) 38 | st := &generator.StructTag{ 39 | Json: &generator.StructTagJson{ 40 | Name: name, 41 | }, 42 | } 43 | if isReferenceType(ctyType) { 44 | st.Json.Omitempty = true 45 | } 46 | return &FieldBuilder{ 47 | f: &generator.Field{ 48 | Name: strcase.ToCamel(name), 49 | Tag: st, 50 | EncodeFnGenerator: encFnGen, 51 | DecodeFnGenerator: decFnGen, 52 | MergeFnGenerator: mergeFnGen, 53 | }, 54 | } 55 | } 56 | 57 | func (fb *FieldBuilder) AttributeField(af generator.AttributeField) *FieldBuilder { 58 | fb.f.Type = generator.FieldTypeAttribute 59 | fb.f.AttributeField = af 60 | return fb 61 | } 62 | 63 | func (fb *FieldBuilder) StructField(typeName string, fields []generator.Field) *FieldBuilder { 64 | fb.f.Type = generator.FieldTypeStruct 65 | // do we need to plumb through package path? that whole concept is getting tiresome 66 | // and isn't used for anything so far 67 | fb.f.StructField = generator.StructField{ 68 | TypeName: typeName, 69 | } 70 | fb.f.Fields = fields 71 | sort.Stable(generator.NamedFields(fb.f.Fields)) 72 | return fb 73 | } 74 | 75 | func (fb *FieldBuilder) IsSlice(is bool) *FieldBuilder { 76 | fb.f.IsSlice = is 77 | return fb 78 | } 79 | 80 | func (fb *FieldBuilder) Unsupported() generator.Field { 81 | return fb.AttributeField( 82 | generator.AttributeField{Type: generator.AttributeTypeUnsupported}).Build() 83 | } 84 | 85 | func (fb *FieldBuilder) ObjectField(typeName string, attrType cty.Type, schemaPath string) *FieldBuilder { 86 | fields := make([]generator.Field, 0) 87 | for k, t := range attrType.ElementType().AttributeTypes() { 88 | fields = append(fields, TypeToField(k, t, schemaPath)) 89 | } 90 | return fb.StructField(typeName, fields) 91 | } 92 | 93 | func (fb *FieldBuilder) Build() generator.Field { 94 | return *fb.f 95 | } 96 | 97 | // TypeToField converts a terraform *configschema.Attribute 98 | // to a crossplane generator.Field 99 | func TypeToField(name string, attrType cty.Type, parentPath string) generator.Field { 100 | sp := appendToSchemaPath(parentPath, name) 101 | fb := NewFieldBuilder(name, attrType) 102 | switch attrType.FriendlyName() { 103 | case "bool": 104 | return fb.AttributeField( 105 | generator.AttributeField{Type: generator.AttributeTypeBool}).Build() 106 | case "number": 107 | return fb.AttributeField( 108 | generator.AttributeField{Type: generator.AttributeTypeInt64}).Build() 109 | case "string": 110 | return fb.AttributeField( 111 | generator.AttributeField{Type: generator.AttributeTypeString}).Build() 112 | case "map of bool": 113 | return fb.AttributeField( 114 | generator.AttributeField{ 115 | Type: generator.AttributeTypeMapStringKey, 116 | MapValueType: generator.AttributeTypeBool, 117 | }).Build() 118 | case "map of number": 119 | return fb.AttributeField( 120 | generator.AttributeField{ 121 | Type: generator.AttributeTypeMapStringKey, 122 | MapValueType: generator.AttributeTypeInt64, 123 | }).Build() 124 | case "map of string": 125 | return fb.AttributeField( 126 | generator.AttributeField{ 127 | Type: generator.AttributeTypeMapStringKey, 128 | MapValueType: generator.AttributeTypeString, 129 | }).Build() 130 | case "list of number": 131 | return fb.IsSlice(true).AttributeField( 132 | generator.AttributeField{Type: generator.AttributeTypeInt64}).Build() 133 | case "list of string": 134 | return fb.IsSlice(true).AttributeField( 135 | generator.AttributeField{Type: generator.AttributeTypeString}).Build() 136 | case "set of number": 137 | return fb.IsSlice(true).AttributeField( 138 | generator.AttributeField{Type: generator.AttributeTypeInt64}).Build() 139 | case "set of string": 140 | return fb.IsSlice(true).AttributeField( 141 | generator.AttributeField{Type: generator.AttributeTypeString}).Build() 142 | case "set of map of string": 143 | return fb.IsSlice(true).AttributeField( 144 | generator.AttributeField{ 145 | Type: generator.AttributeTypeMapStringKey, 146 | MapValueType: generator.AttributeTypeString, 147 | }).Build() 148 | 149 | // TODO: the set/list of objects types can probably be []map[string]string 150 | // but we need to spot check and confirm this. 151 | case "list of object": // TODO: probably can be []map[string]string 152 | //f.AttributeField.Type = generator.AttributeTypeUnsupported 153 | // TODO: see note on "set of object" re object schemas, this may also apply 154 | // to constructing lists of object 155 | if !attrType.IsListType() { 156 | return fb.Unsupported() 157 | } 158 | if !attrType.ElementType().IsObjectType() { 159 | return fb.Unsupported() 160 | } 161 | return fb.IsSlice(true).ObjectField(strcase.ToCamel(name), attrType, sp).Build() 162 | case "set of object": 163 | // TODO: sets of objects have a fixed schema that we need to track in order to 164 | // marshal them later. I think it may be an error to try to declare a set with 165 | // an object definition consisting just of set fields (a subset of the fields) 166 | // i've also seen (in provider configs) errors when optional set types are not 167 | // declared. look at the provider config construction in provider_terraform_aws 168 | // for an example. 169 | if !attrType.IsSetType() { 170 | return fb.Unsupported() 171 | } 172 | if !attrType.ElementType().IsObjectType() { 173 | return fb.Unsupported() 174 | } 175 | return fb.IsSlice(true).ObjectField(strcase.ToCamel(name), attrType, sp).Build() 176 | default: 177 | // TODO: need better error handling here to help generate error messages 178 | // which would describe why the field is unsupported 179 | // maybe this panic, either here or further up the stack 180 | return fb.Unsupported() 181 | } 182 | } 183 | 184 | func SpecOrStatus(attr *configschema.Attribute) SpecOrStatusField { 185 | // if attr.Computed is true, it can either be an attribute (status) or an argument (spec) 186 | // but arguments will always either be required or optional 187 | if attr.Required || attr.Optional { 188 | return ForProviderField 189 | } 190 | return AtProviderField 191 | } 192 | 193 | func appendToSchemaPath(sp, name string) string { 194 | return fmt.Sprintf("%s_%s", sp, name) 195 | } 196 | 197 | // SpecStatusAttributeFields iterates through the terraform configschema.Attribute map 198 | // found under Block.Attributes, translating each attribute to a generator.Field and 199 | // grouping them as spec or status based on their optional/required/computed properties. 200 | func SpecOrStatusAttributeFields(attributes map[string]*configschema.Attribute, namer generator.ResourceNamer) ([]generator.Field, []generator.Field) { 201 | forProvider := make([]generator.Field, 0) 202 | atProvider := make([]generator.Field, 0) 203 | forProviderPath := fmt.Sprintf("%s_%s_%s", namer.TypeName(), namer.SpecTypeName(), namer.ForProviderTypeName()) 204 | atProviderPath := fmt.Sprintf("%s_%s_%s", namer.TypeName(), namer.StatusTypeName(), namer.AtProviderTypeName()) 205 | for name, attr := range attributes { 206 | // filter the top-level terraform id field out of the schema, these are 207 | // manually handled in the generated encode/decode methods 208 | if name == "id" { 209 | continue 210 | } 211 | switch SpecOrStatus(attr) { 212 | case ForProviderField: 213 | f := TypeToField(name, attr.Type, forProviderPath) 214 | forProvider = append(forProvider, f) 215 | case AtProviderField: 216 | f := TypeToField(name, attr.Type, atProviderPath) 217 | atProvider = append(atProvider, f) 218 | } 219 | } 220 | sort.Stable(generator.NamedFields(forProvider)) 221 | sort.Stable(generator.NamedFields(atProvider)) 222 | return forProvider, atProvider 223 | } 224 | 225 | var ( 226 | ctyListCollectionType = cty.List(cty.EmptyObject) 227 | ctySetCollectionType = cty.Set(cty.EmptyObject) 228 | ctyMapCollectionType = cty.Map(cty.EmptyObject) 229 | ) 230 | 231 | func NestedBlockFields(blocks map[string]*configschema.NestedBlock, packagePath, schemaPath string) []generator.Field { 232 | fields := make([]generator.Field, 0) 233 | for name, block := range blocks { 234 | f := generator.Field{ 235 | Name: strcase.ToCamel(name), 236 | Fields: make([]generator.Field, 0), 237 | Type: generator.FieldTypeStruct, 238 | StructField: generator.StructField{ 239 | PackagePath: packagePath, 240 | // TODO: the output would look nicer if we pluralized names when IsBlockList is true 241 | TypeName: strcase.ToCamel(name), 242 | }, 243 | Tag: &generator.StructTag{ 244 | Json: &generator.StructTagJson{ 245 | Name: name, 246 | }, 247 | }, 248 | Required: IsBlockRequired(block), 249 | IsSlice: IsBlockSlice(block), 250 | } 251 | f.EncodeFnGenerator = NewBlockEncodeFnGenerator(name, block) 252 | f.DecodeFnGenerator = NewBlockDecodeFnGenerator(name, block) 253 | f.MergeFnGenerator = NewBlockMergeFnGenerator(name, block) 254 | 255 | sp := appendToSchemaPath(schemaPath, f.Name) 256 | for n, attr := range block.Attributes { 257 | f.Fields = append(f.Fields, TypeToField(n, attr.Type, sp)) 258 | } 259 | sort.Stable(generator.NamedFields(f.Fields)) 260 | f.Fields = append(f.Fields, NestedBlockFields(block.BlockTypes, packagePath, sp)...) 261 | fields = append(fields, f) 262 | } 263 | sort.Stable(generator.NamedFields(fields)) 264 | return fields 265 | } 266 | 267 | func SchemaToManagedResource(name, packagePath string, s providers.Schema) *generator.ManagedResource { 268 | namer := generator.NewDefaultNamer(strcase.ToCamel(name)) 269 | mr := generator.NewManagedResource(namer.TypeName(), packagePath).WithNamer(namer) 270 | spec, status := SpecOrStatusAttributeFields(s.Block.Attributes, namer) 271 | mr.Parameters = generator.Field{ 272 | Tag: &generator.StructTag{ 273 | Json: &generator.StructTagJson{ 274 | Name: "forProvider", 275 | }, 276 | }, 277 | Type: generator.FieldTypeStruct, 278 | StructField: generator.StructField{ 279 | PackagePath: packagePath, 280 | TypeName: namer.ForProviderTypeName(), 281 | }, 282 | Fields: spec, 283 | Name: namer.ForProviderTypeName(), 284 | } 285 | mr.Observation = generator.Field{ 286 | Tag: &generator.StructTag{ 287 | Json: &generator.StructTagJson{ 288 | Name: "atProvider", 289 | }, 290 | }, 291 | Type: generator.FieldTypeStruct, 292 | StructField: generator.StructField{ 293 | PackagePath: packagePath, 294 | TypeName: namer.AtProviderTypeName(), 295 | }, 296 | Fields: status, 297 | Name: namer.AtProviderTypeName(), 298 | } 299 | nb := NestedBlockFields(s.Block.BlockTypes, packagePath, namer.TypeName()) 300 | if len(nb) > 0 { 301 | // currently the assumption is that the nested types are spec fields 302 | // TODO: write an analyzer to ensure that deeply nested types are not common in status 303 | // we could do tree search into the structure of a NestedBlock 304 | mr.Parameters.Fields = append(mr.Parameters.Fields, nb...) 305 | } 306 | return mr 307 | } 308 | 309 | func IsBlockRequired(nb *configschema.NestedBlock) bool { 310 | if nb.MinItems > 0 { 311 | return true 312 | } 313 | return false 314 | } 315 | 316 | func IsBlockSlice(nb *configschema.NestedBlock) bool { 317 | // this is used to indicate a single optional block, like aws_db_proxy.timeouts 318 | // idk why it does not have a MaxItems = 1 319 | // take a look w/: 320 | // ./terraform-provider-gen analyze --plugin-path=$PLUGIN_PATH --providerName=$PROVIDER_NAME nesting | grep 'NestingList (0, 0' 321 | if nb.MaxItems == 0 && nb.MinItems == 0 { 322 | return false 323 | } 324 | if nb.MaxItems != 1 { 325 | return true 326 | } 327 | return false 328 | } 329 | -------------------------------------------------------------------------------- /provider-configs/aws.yaml: -------------------------------------------------------------------------------- 1 | name: aws 2 | base-crd-version: v1alpha1 3 | package-path: github.com/crossplane-contrib/provider-terraform-aws/generated/resources 4 | exclude-resources: 5 | # lambda alias has a nested map[string]float structure that confuses codegen 6 | # this is actually a surprising bug, needs more investigation 7 | - aws_lambda_alias 8 | # all of the following resources have duplicate nested fields, which creates 9 | # duplicate/conflicting struct names. 10 | # the plan to handle these is to add a stage to code generation where we 11 | # recursively search the field tree, renaming duplicates first-in-first-out, 12 | # by walking back up the tree and prepending parent names 13 | # until the result is unique 14 | # note: we can also determine whether collisions are duplicates or conflicting 15 | # but this is a bit more work and should be safe to leave as an improvement. 16 | # for 'aws_acm_certificate' and all following resources, we can generate types.go and encode.go 17 | # but decode.go needs a little bit more work, excluding them until we can wrap 18 | # them up so that we can get this moving with a limited set of types 19 | - aws_kinesis_firehose_delivery_stream 20 | - aws_appmesh_route 21 | - aws_wafv2_rule_group 22 | - aws_emr_cluster 23 | - aws_wafv2_web_acl 24 | - aws_kinesis_analytics_application 25 | - aws_autoscaling_group 26 | - aws_appsync_graphql_api 27 | - aws_iot_topic_rule 28 | - aws_ssm_maintenance_window_task 29 | - aws_cloudfront_distribution 30 | - aws_codedeploy_deployment_group 31 | - aws_lex_bot 32 | - aws_codebuild_project 33 | - aws_backup_plan 34 | - aws_appmesh_virtual_node 35 | - aws_lex_intent 36 | - aws_s3_bucket 37 | - aws_acm_certificate 38 | - aws_acmpca_certificate_authority 39 | - aws_alb 40 | - aws_alb_listener 41 | - aws_alb_listener_rule 42 | - aws_alb_target_group 43 | - aws_ami 44 | - aws_ami_copy 45 | - aws_ami_from_instance 46 | - aws_api_gateway_account 47 | - aws_api_gateway_documentation_part 48 | - aws_api_gateway_domain_name 49 | - aws_api_gateway_method 50 | - aws_api_gateway_method_response 51 | - aws_api_gateway_method_settings 52 | - aws_api_gateway_rest_api 53 | - aws_api_gateway_stage 54 | - aws_api_gateway_usage_plan 55 | - aws_apigatewayv2_api 56 | - aws_apigatewayv2_authorizer 57 | - aws_apigatewayv2_domain_name 58 | - aws_apigatewayv2_integration 59 | - aws_apigatewayv2_stage 60 | - aws_appautoscaling_policy 61 | - aws_appautoscaling_scheduled_action 62 | - aws_appmesh_mesh 63 | - aws_appmesh_virtual_router 64 | - aws_appmesh_virtual_service 65 | - aws_appsync_datasource 66 | - aws_appsync_resolver 67 | - aws_athena_database 68 | - aws_athena_workgroup 69 | - aws_autoscaling_policy 70 | - aws_backup_selection 71 | - aws_batch_compute_environment 72 | - aws_batch_job_definition 73 | - aws_budgets_budget 74 | - aws_cloudhsm_v2_cluster 75 | - aws_cloudtrail 76 | - aws_cloudwatch_event_permission 77 | - aws_cloudwatch_event_target 78 | - aws_cloudwatch_log_metric_filter 79 | - aws_cloudwatch_metric_alarm 80 | - aws_codeartifact_repository 81 | - aws_codebuild_report_group 82 | - aws_codebuild_webhook 83 | - aws_codecommit_trigger 84 | - aws_codedeploy_deployment_config 85 | - aws_codepipeline 86 | - aws_codepipeline_webhook 87 | - aws_codestarnotifications_notification_rule 88 | - aws_cognito_identity_pool 89 | - aws_cognito_identity_pool_roles_attachment 90 | - aws_cognito_resource_server 91 | - aws_cognito_user_pool 92 | - aws_cognito_user_pool_client 93 | - aws_config_config_rule 94 | - aws_config_configuration_aggregator 95 | - aws_config_configuration_recorder_status 96 | - aws_config_configuration_recorder 97 | - aws_config_delivery_channel 98 | - aws_config_remediation_configuration 99 | - aws_datasync_agent 100 | - aws_datasync_location_efs 101 | - aws_datasync_location_nfs 102 | - aws_datasync_location_s3 103 | - aws_datasync_location_smb 104 | - aws_datasync_task 105 | - aws_dax_cluster 106 | - aws_dax_parameter_group 107 | - aws_db_instance 108 | - aws_db_option_group 109 | - aws_db_parameter_group 110 | - aws_db_proxy 111 | - aws_db_proxy_default_target_group 112 | - aws_db_security_group 113 | - aws_default_network_acl 114 | - aws_default_route_table 115 | - aws_default_security_group 116 | - aws_directory_service_directory 117 | - aws_dlm_lifecycle_policy 118 | - aws_dms_endpoint 119 | - aws_docdb_cluster_parameter_group 120 | - aws_dynamodb_global_table 121 | - aws_dynamodb_table 122 | - aws_ec2_client_vpn_endpoint 123 | - aws_ec2_fleet 124 | - aws_ec2_traffic_mirror_filter_rule 125 | - aws_ecr_repository 126 | - aws_ecs_capacity_provider 127 | - aws_ecs_cluster 128 | - aws_ecs_service 129 | - aws_ecs_task_definition 130 | - aws_efs_access_point 131 | - aws_efs_file_system 132 | - aws_eks_cluster 133 | - aws_eks_fargate_profile 134 | - aws_eks_node_group 135 | - aws_elastic_beanstalk_application 136 | - aws_elastic_beanstalk_application_version 137 | - aws_elastic_beanstalk_configuration_template 138 | - aws_elastic_beanstalk_environment 139 | - aws_elasticache_cluster 140 | - aws_elasticache_parameter_group 141 | - aws_elasticache_replication_group 142 | - aws_elasticsearch_domain 143 | - aws_elastictranscoder_pipeline 144 | - aws_elastictranscoder_preset 145 | - aws_elb 146 | - aws_emr_instance_fleet 147 | - aws_emr_instance_group 148 | - aws_emr_managed_scaling_policy 149 | - aws_fsx_windows_file_system 150 | - aws_gamelift_alias 151 | - aws_gamelift_build 152 | - aws_gamelift_fleet 153 | - aws_gamelift_game_session_queue 154 | - aws_glacier_vault 155 | - aws_globalaccelerator_accelerator 156 | - aws_globalaccelerator_endpoint_group 157 | - aws_globalaccelerator_listener 158 | - aws_glue_catalog_table 159 | - aws_glue_classifier 160 | - aws_glue_connection 161 | - aws_glue_crawler 162 | - aws_glue_data_catalog_encryption_settings 163 | - aws_glue_job 164 | - aws_glue_ml_transform 165 | - aws_glue_partition 166 | - aws_glue_security_configuration 167 | - aws_glue_trigger 168 | - aws_glue_user_defined_function 169 | - aws_guardduty_filter 170 | - aws_instance 171 | - aws_iot_thing_type 172 | - aws_kms_grant 173 | - aws_lambda_event_source_mapping 174 | - aws_lambda_function 175 | - aws_lambda_function_event_invoke_config 176 | - aws_launch_configuration 177 | - aws_launch_template 178 | - aws_lb 179 | - aws_lb_listener 180 | - aws_lb_listener_rule 181 | - aws_lb_ssl_negotiation_policy 182 | - aws_lb_target_group 183 | - aws_lex_slot_type 184 | - aws_load_balancer_policy 185 | - aws_macie_s3_bucket_association 186 | - aws_media_convert_queue 187 | - aws_media_package_channel 188 | - aws_mq_broker 189 | - aws_msk_cluster 190 | - aws_neptune_cluster_parameter_group 191 | - aws_neptune_parameter_group 192 | - aws_network_acl 193 | - aws_network_interface 194 | - aws_opsworks_application 195 | - aws_opsworks_custom_layer 196 | - aws_opsworks_ganglia_layer 197 | - aws_opsworks_haproxy_layer 198 | - aws_opsworks_instance 199 | - aws_opsworks_java_app_layer 200 | - aws_opsworks_memcached_layer 201 | - aws_opsworks_mysql_layer 202 | - aws_opsworks_nodejs_app_layer 203 | - aws_opsworks_php_app_layer 204 | - aws_opsworks_rails_app_layer 205 | - aws_opsworks_stack 206 | - aws_opsworks_static_web_layer 207 | - aws_organizations_organization 208 | - aws_organizations_organizational_unit 209 | - aws_pinpoint_event_stream 210 | - aws_pinpoint_app 211 | - aws_rds_cluster 212 | - aws_rds_cluster_parameter_group 213 | - aws_rds_global_cluster 214 | - aws_redshift_cluster 215 | - aws_redshift_parameter_group 216 | - aws_redshift_security_group 217 | - aws_resourcegroups_group 218 | - aws_route53_record 219 | - aws_route53_resolver_endpoint 220 | - aws_route53_resolver_rule 221 | - aws_route53_zone 222 | - aws_route_table 223 | - aws_s3_access_point 224 | - aws_s3_bucket_analytics_configuration 225 | - aws_s3_bucket_inventory 226 | - aws_s3_bucket_metric 227 | - aws_s3_bucket_notification 228 | - aws_sagemaker_endpoint_configuration 229 | - aws_sagemaker_model 230 | - aws_secretsmanager_secret 231 | - aws_secretsmanager_secret_rotation 232 | - aws_security_group 233 | - aws_service_discovery_service 234 | - aws_ses_event_destination 235 | - aws_ses_receipt_rule 236 | - aws_spot_fleet_request 237 | - aws_spot_instance_request 238 | - aws_ssm_association 239 | - aws_ssm_document 240 | - aws_ssm_maintenance_window_target 241 | - aws_ssm_patch_baseline 242 | - aws_ssm_resource_data_sync 243 | - aws_storagegateway_gateway 244 | - aws_storagegateway_nfs_file_share 245 | - aws_storagegateway_smb_file_share 246 | - aws_transfer_server 247 | - aws_transfer_user 248 | - aws_vpc_endpoint 249 | - aws_vpc_peering_connection 250 | - aws_vpc_peering_connection_accepter 251 | - aws_vpc_peering_connection_options 252 | - aws_vpn_connection 253 | - aws_waf_byte_match_set 254 | - aws_waf_geo_match_set 255 | - aws_waf_ipset 256 | - aws_waf_rate_based_rule 257 | - aws_waf_regex_match_set 258 | - aws_waf_rule 259 | - aws_waf_rule_group 260 | - aws_waf_size_constraint_set 261 | - aws_waf_sql_injection_match_set 262 | - aws_waf_web_acl 263 | - aws_waf_xss_match_set 264 | - aws_wafregional_byte_match_set 265 | - aws_wafregional_geo_match_set 266 | - aws_wafregional_ipset 267 | - aws_wafregional_rate_based_rule 268 | - aws_wafregional_regex_match_set 269 | - aws_wafregional_rule 270 | - aws_wafregional_rule_group 271 | - aws_wafregional_size_constraint_set 272 | - aws_wafregional_sql_injection_match_set 273 | - aws_wafregional_web_acl 274 | - aws_wafregional_xss_match_set 275 | - aws_wafv2_regex_pattern_set 276 | - aws_wafv2_web_acl_logging_configuration 277 | - aws_worklink_fleet 278 | - aws_workspaces_directory 279 | - aws_workspaces_ip_group 280 | - aws_workspaces_workspace -------------------------------------------------------------------------------- /provider-configs/vsphere.yaml: -------------------------------------------------------------------------------- 1 | name: vsphere 2 | base-crd-version: v1alpha1 3 | root-package: github.com/crossplane-contrib/provider-terraform-vsphere 4 | package-path: github.com/crossplane-contrib/provider-terraform-vsphere/generated/resources 5 | base-path: /Users/kasey/src/crossplane-contrib/provider-terraform-vsphere/ 6 | provider-config-version: v1alpha1 7 | api-group: vsphere.terraform-plugin.crossplane.io 8 | --------------------------------------------------------------------------------