Skip to content

Commit f93d141

Browse files
authored
Merge pull request #506 from shiftstack/scaffold-controller
Controller generator
2 parents 20ee050 + 5c76eac commit f93d141

File tree

78 files changed

+2768
-53
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+2768
-53
lines changed

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ generate-codegen: generate-controller-gen ## codegen requires DeepCopy etc
7676
generate-go: mockgen
7777
go generate ./...
7878

79+
.PHONY: generate-bundle
80+
generate-bundle: kustomize operator-sdk
81+
./hack/bundle.sh
82+
7983
.PHONY: generate-docs
8084
generate-docs:
8185
$(MAKE) -C website generated
@@ -209,8 +213,7 @@ build-installer: manifests generate kustomize ## Generate a consolidated YAML wi
209213
$(KUSTOMIZE) build $(CUSTOMDEPLOY) > dist/install.yaml
210214

211215
.PHONY: build-bundle-image
212-
build-bundle-image: kustomize operator-sdk
213-
bash hack/bundle.sh
216+
build-bundle-image: generate-bundle
214217
$(CONTAINER_TOOL) build -f bundle.Dockerfile -t ${BUNDLE_IMG} .
215218

216219
##@ Deployment

PROJECT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Code generated by tool. DO NOT EDIT.
1+
# Code generated by resource-generator. DO NOT EDIT.
22
# This file is used to track the info used to scaffold your project
33
# and allow the plugins properly work.
44
# More info: https://book.kubebuilder.io/reference/project-config.html
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# This file is used to track the info used to scaffold your project
2+
# and allow the plugins properly work.
3+
# More info: https://book.kubebuilder.io/reference/project-config.html
4+
domain: k-orc.cloud
5+
layout:
6+
- go.kubebuilder.io/v4
7+
projectName: orc
8+
repo: github.com/k-orc/openstack-resource-controller
9+
resources:
10+
{{- range . }}
11+
- api:
12+
crdVersion: v1
13+
namespaced: true
14+
domain: k-orc.cloud
15+
group: openstack
16+
kind: {{ .Name }}
17+
path: github.com/k-orc/openstack-resource-controller/api/{{ .APIVersion }}
18+
version: {{ .APIVersion }}
19+
{{- end}}
20+
version: "3"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# This kustomization.yaml is not intended to be run by itself,
2+
# since it depends on service name and namespace that are out of this kustomize package.
3+
# It should be run by config/default
4+
resources:
5+
{{- range . }}
6+
- bases/openstack.k-orc.cloud_{{ .NameLower }}s.yaml
7+
{{- end}}
8+
# +kubebuilder:scaffold:crdkustomizeresource
9+
10+
patches:
11+
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
12+
# patches here are for enabling the conversion webhook for each CRD
13+
# +kubebuilder:scaffold:crdkustomizewebhookpatch
14+
15+
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
16+
# patches here are for enabling the CA injection for each CRD
17+
#- path: patches/cainjection_in_images.yaml
18+
# +kubebuilder:scaffold:crdkustomizecainjectionpatch
19+
20+
# [WEBHOOK] To enable webhook, uncomment the following section
21+
# the following config is for teaching kustomize how to do kustomization for CRDs.
22+
23+
#configurations:
24+
#- kustomizeconfig.yaml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## Append samples of your project ##
2+
resources:
3+
{{- range . }}
4+
- openstack_{{ .APIVersion }}_{{ .NameLower }}.yaml
5+
{{- end}}
6+
# +kubebuilder:scaffold:manifestskustomizesamples
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2025 The ORC 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 mock
18+
19+
import (
20+
// Runtime dependency of mockgen, required when using vendoring so go mod knows
21+
// to pull it in.
22+
_ "go.uber.org/mock/mockgen/model"
23+
)
24+
25+
//go:generate mockgen -package mock -destination=compute.go -source=../compute.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock ComputeClient
26+
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt compute.go > _compute.go && mv _compute.go compute.go"
27+
28+
//go:generate mockgen -package mock -destination=image.go -source=../image.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock ImageClient
29+
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt image.go > _image.go && mv _image.go image.go"
30+
31+
//go:generate mockgen -package mock -destination=networking.go -source=../networking.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock NetworkClient
32+
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt networking.go > _networking.go && mv _networking.go networking.go"
33+
34+
//go:generate mockgen -package mock -destination=identity.go -source=../identity.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock IdentityClient
35+
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt identity.go > _identity.go && mv _identity.go identity.go"
36+
{{- range . }}
37+
{{- if not .ExistingOSClient }}
38+
39+
//go:generate mockgen -package mock -destination={{ .NameLower }}.go -source=../{{ .NameLower }}.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock {{ .Name }}Client
40+
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt {{ .NameLower }}.go > _{{ .NameLower }}.go && mv _{{ .NameLower }}.go {{ .NameLower }}.go"
41+
{{- end}}
42+
{{- end}}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: kuttl.dev/v1beta1
2+
kind: TestSuite
3+
testDirs:
4+
{{- range . }}
5+
- ./internal/controllers/{{ .NameLower }}/tests/
6+
{{- end}}
7+
timeout: 120

cmd/resource-generator/main.go

Lines changed: 138 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"os"
77
"path/filepath"
8+
"slices"
9+
"sort"
810
"strings"
911
"text/template"
1012
)
@@ -23,6 +25,21 @@ var adapter_template string
2325
//go:embed data/controller.template
2426
var controller_template string
2527

28+
//go:embed data/PROJECT.template
29+
var project_template string
30+
31+
//go:embed data/kuttl-test.yaml.template
32+
var kuttl_test_template string
33+
34+
//go:embed data/config-crd-kustomization.yaml.template
35+
var crd_kustomization_template string
36+
37+
//go:embed data/config-samples-kustomization.yaml.template
38+
var samples_kustomization_template string
39+
40+
//go:embed data/internal-osclients-mock-doc.go.template
41+
var mock_doc_template string
42+
2643
type specExtraValidation struct {
2744
Rule string
2845
Message string
@@ -45,22 +62,15 @@ type templateFields struct {
4562
StatusExtraType string
4663
SpecExtraValidations []specExtraValidation
4764
AdditionalPrintColumns []additionalPrintColumn
65+
// NOTE: this is temporary until we migrate the controllers to use a dedicated client
66+
// New controllers should not set this field
67+
ExistingOSClient bool
4868
}
4969

50-
var allResources []templateFields = []templateFields{
70+
var resources []templateFields = []templateFields{
5171
{
52-
Name: "Image",
53-
SpecExtraValidations: []specExtraValidation{
54-
{
55-
Rule: "!has(self.__import__) ? has(self.resource.content) : true",
56-
Message: "resource content must be specified when not importing",
57-
},
58-
},
59-
StatusExtraType: "ImageStatusExtra",
60-
},
61-
{
62-
Name: "Flavor",
63-
APIVersion: "v1alpha1",
72+
Name: "Flavor",
73+
ExistingOSClient: true,
6474
},
6575
{
6676
Name: "FloatingIP",
@@ -72,17 +82,23 @@ var allResources []templateFields = []templateFields{
7282
Description: "Allocated IP address",
7383
},
7484
},
75-
IsNotNamed: true, // FloatingIP is not named in OpenStack
76-
},
77-
{
78-
Name: "Network",
79-
APIVersion: "v1alpha1",
85+
IsNotNamed: true, // FloatingIP is not named in OpenStack
86+
ExistingOSClient: true,
8087
},
8188
{
82-
Name: "Subnet",
89+
Name: "Image",
90+
SpecExtraValidations: []specExtraValidation{
91+
{
92+
Rule: "!has(self.__import__) ? has(self.resource.content) : true",
93+
Message: "resource content must be specified when not importing",
94+
},
95+
},
96+
StatusExtraType: "ImageStatusExtra",
97+
ExistingOSClient: true,
8398
},
8499
{
85-
Name: "Router",
100+
Name: "Network",
101+
ExistingOSClient: true,
86102
},
87103
{
88104
Name: "Port",
@@ -94,46 +110,65 @@ var allResources []templateFields = []templateFields{
94110
Description: "Allocated IP addresses",
95111
},
96112
},
113+
ExistingOSClient: true,
114+
},
115+
{
116+
Name: "Project",
117+
ExistingOSClient: true,
97118
},
98119
{
99-
Name: "SecurityGroup",
120+
Name: "Router",
121+
ExistingOSClient: true,
100122
},
101123
{
102-
Name: "Server",
124+
Name: "SecurityGroup",
125+
ExistingOSClient: true,
103126
},
104127
{
105-
Name: "ServerGroup",
128+
Name: "Server",
129+
ExistingOSClient: true,
106130
},
107131
{
108-
Name: "Project",
132+
Name: "ServerGroup",
133+
ExistingOSClient: true,
134+
},
135+
{
136+
Name: "Subnet",
137+
ExistingOSClient: true,
138+
},
139+
}
140+
141+
// These resources won't be generated
142+
var specialResources []templateFields = []templateFields{
143+
{
144+
Name: "RouterInterface",
145+
ExistingOSClient: true,
109146
},
110147
}
111148

112149
func main() {
113150
apiTemplate := template.Must(template.New("api").Parse(api_template))
114151
adapterTemplate := template.Must(template.New("adapter").Parse(adapter_template))
115152
controllerTemplate := template.Must(template.New("controller").Parse(controller_template))
153+
projectTemplate := template.Must(template.New("project").Parse(project_template))
154+
kuttlTestTemplate := template.Must(template.New("kuttl-test").Parse(kuttl_test_template))
155+
crdKustomizationTemplate := template.Must(template.New("crd-kustomization").Parse(crd_kustomization_template))
156+
samplesKustomizationTemplate := template.Must(
157+
template.New("samples-kustomization").Parse(samples_kustomization_template))
158+
mockDocTemplate := template.Must(template.New("mock-doc").Parse(mock_doc_template))
116159

117-
for i := range allResources {
118-
resource := &allResources[i]
160+
addDefaults(resources)
161+
addDefaults(specialResources)
119162

120-
if resource.Year == "" {
121-
resource.Year = defaultYear
122-
}
163+
for i := range resources {
164+
resource := &resources[i]
123165

124-
if resource.APIVersion == "" {
125-
resource.APIVersion = defaultAPIVersion
126-
}
127-
128-
resourceLower := strings.ToLower(resource.Name)
129-
resource.NameLower = resourceLower
130-
131-
apiPath := filepath.Join("api", resource.APIVersion, "zz_generated."+resourceLower+"-resource.go")
166+
apiPath := filepath.Join("api", resource.APIVersion, "zz_generated."+resource.NameLower+"-resource.go")
132167
if err := writeTemplate(apiPath, apiTemplate, resource); err != nil {
133168
panic(err)
134169
}
135170

136-
controllerDirPath := filepath.Join("internal", "controllers", resourceLower)
171+
controllerDirPath := filepath.Join("internal", "controllers", resource.NameLower)
137172
if _, err := os.Stat(controllerDirPath); os.IsNotExist(err) {
138173
err = os.Mkdir(controllerDirPath, 0755)
139174
if err != nil {
@@ -151,9 +186,59 @@ func main() {
151186
panic(err)
152187
}
153188
}
189+
190+
// NOTE: some resources needs special handling.
191+
// Let's add them now and sort the resulting slice alphabetically by resource name.
192+
allResources := slices.Concat(resources, specialResources)
193+
sort.Slice(allResources, func(i, j int) bool {
194+
return allResources[i].Name < allResources[j].Name
195+
})
196+
197+
if err := writeTemplate("PROJECT", projectTemplate, allResources); err != nil {
198+
panic(err)
199+
}
200+
201+
if err := writeTemplate("kuttl-test.yaml", kuttlTestTemplate, allResources); err != nil {
202+
panic(err)
203+
}
204+
205+
crdKustomizationPath := filepath.Join("config", "crd", "kustomization.yaml")
206+
if err := writeTemplate(crdKustomizationPath, crdKustomizationTemplate, allResources); err != nil {
207+
panic(err)
208+
}
209+
210+
samplesKustomizationPath := filepath.Join("config", "samples", "kustomization.yaml")
211+
if err := writeTemplate(samplesKustomizationPath, samplesKustomizationTemplate, allResources); err != nil {
212+
panic(err)
213+
}
214+
215+
mockDocPath := filepath.Join("internal", "osclients", "mock", "doc.go")
216+
if err := writeTemplate(mockDocPath, mockDocTemplate, allResources); err != nil {
217+
panic(err)
218+
}
154219
}
155220

156-
func writeTemplate(path string, template *template.Template, resource *templateFields) (err error) {
221+
func addDefaults(resources []templateFields) {
222+
for i := range resources {
223+
resource := &resources[i]
224+
225+
if resource.Year == "" {
226+
resource.Year = defaultYear
227+
}
228+
229+
if resource.APIVersion == "" {
230+
resource.APIVersion = defaultAPIVersion
231+
}
232+
233+
resource.NameLower = strings.ToLower(resource.Name)
234+
}
235+
}
236+
237+
type ResourceType interface {
238+
*templateFields | []templateFields
239+
}
240+
241+
func writeTemplate[T ResourceType](path string, tmpl *template.Template, resource T) (err error) {
157242
file, err := os.Create(path)
158243
if err != nil {
159244
return err
@@ -167,11 +252,23 @@ func writeTemplate(path string, template *template.Template, resource *templateF
167252
return err
168253
}
169254

170-
return template.Execute(file, resource)
255+
return tmpl.Execute(file, resource)
171256
}
172257

173258
func writeAutogeneratedHeader(f *os.File) error {
174-
_, err := f.WriteString("// Code generated by resource-generator. DO NOT EDIT.\n")
259+
var commentPrefix string
260+
261+
switch filepath.Ext(f.Name()) {
262+
case ".go":
263+
commentPrefix = "//"
264+
case ".yaml", ".yml":
265+
commentPrefix = "#"
266+
default:
267+
commentPrefix = "#"
268+
}
269+
270+
header := commentPrefix + " Code generated by resource-generator. DO NOT EDIT.\n"
271+
_, err := f.WriteString(header)
175272

176273
return err
177274
}

0 commit comments

Comments
 (0)