Skip to content

Commit 1274edd

Browse files
committed
initial
1 parent 4cbc679 commit 1274edd

File tree

10 files changed

+581
-185
lines changed

10 files changed

+581
-185
lines changed

Tiltfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ providers = {
142142
# Add the ExtensionConfig for this Runtime extension; given that the ExtensionConfig can be installed only when capi_controller
143143
# are up and running, it is required to set a resource_deps to ensure proper install order.
144144
"additional_resources": [
145-
"config/tilt/extensionconfig.yaml",
145+
"tilt/extensionconfig.yaml",
146146
],
147147
"resource_deps": ["capi_controller"],
148148
},

exp/runtime/topologymutation/walker.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package topologymutation
1919
import (
2020
"context"
2121
"encoding/json"
22+
"fmt"
2223

2324
mergepatch "github.com/evanphx/json-patch/v5"
2425
"github.com/pkg/errors"
@@ -79,8 +80,8 @@ func (d PatchFormat) ApplyToWalkTemplates(in *WalkTemplatesOptions) {
7980
// Also, by using this func it is possible to ignore most of the details of the GeneratePatchesRequest
8081
// and GeneratePatchesResponse messages format and focus on writing patches/modifying the templates.
8182
func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesRequest,
82-
resp *runtimehooksv1.GeneratePatchesResponse, mutateFunc func(ctx context.Context, obj runtime.Object,
83-
variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error, opts ...WalkTemplatesOption) {
83+
resp *runtimehooksv1.GeneratePatchesResponse, variablesType interface{}, mutateFunc func(ctx context.Context, obj runtime.Object,
84+
builtinVariable *patchvariables.Builtins, variables interface{}, holderRef runtimehooksv1.HolderReference) error, opts ...WalkTemplatesOption) {
8485
log := ctrl.LoggerFrom(ctx)
8586
globalVariables := patchvariables.ToMap(req.Variables)
8687

@@ -91,6 +92,7 @@ func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehoo
9192

9293
// For all the templates in a request.
9394
// TODO: add a notion of ordering the patch implementers can rely on. Ideally ordering could be pluggable via options.
95+
// An alternative is to provide functions to retrieve specific "templates", e.g. GetControlPlaneTemplate.
9496
for _, requestItem := range req.Items {
9597
// Computes the variables that apply to the template, by merging global and template variables.
9698
templateVariables, err := patchvariables.MergeVariableMaps(globalVariables, patchvariables.ToMap(requestItem.Variables))
@@ -100,6 +102,35 @@ func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehoo
100102
return
101103
}
102104

105+
// FIXME: let's do this for all variables before we actually call `mutateFunc`
106+
// FIXME: convert variable values to go types
107+
// godoc, handle errors, ...
108+
variableValuesJSON := map[string]apiextensionsv1.JSON{}
109+
var builtinVariableJSON apiextensionsv1.JSON
110+
for variableName, variableValue := range templateVariables {
111+
if variableName == patchvariables.BuiltinsName {
112+
builtinVariableJSON = variableValue
113+
continue
114+
}
115+
116+
variableValuesJSON[variableName] = variableValue
117+
}
118+
119+
variableValuesJSONAll, err := json.Marshal(variableValuesJSON)
120+
if err != nil {
121+
fmt.Println(err)
122+
}
123+
124+
var builtinVariableValue patchvariables.Builtins
125+
if err := json.Unmarshal(builtinVariableJSON.Raw, &builtinVariableValue); err != nil {
126+
fmt.Println(err)
127+
}
128+
129+
// FIXME: just using variablesType directly is probably not clean enough
130+
if err := json.Unmarshal(variableValuesJSONAll, &variablesType); err != nil {
131+
fmt.Println(err)
132+
}
133+
103134
// Convert the template object into a typed object.
104135
original, _, err := decoder.Decode(requestItem.Object.Raw, nil, requestItem.Object.Object)
105136
if err != nil {
@@ -138,7 +169,7 @@ func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehoo
138169
// Calls the mutateFunc.
139170
requestItemLog.V(4).Info("Generating patch for template")
140171
modified := original.DeepCopyObject()
141-
if err := mutateFunc(requestItemCtx, modified, templateVariables, requestItem.HolderReference); err != nil {
172+
if err := mutateFunc(requestItemCtx, modified, &builtinVariableValue, variablesType, requestItem.HolderReference); err != nil {
142173
resp.Status = runtimehooksv1.ResponseStatusFailure
143174
resp.Message = err.Error()
144175
return

exp/runtime/topologymutation/walker_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func Test_WalkTemplates(t *testing.T) {
5151
controlplanev1.GroupVersion,
5252
bootstrapv1.GroupVersion,
5353
)
54-
mutatingFunc := func(ctx context.Context, obj runtime.Object, variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error {
54+
mutatingFunc := func(ctx context.Context, obj runtime.Object, builtinVariable *variables.Builtins, variables interface{}, holderRef runtimehooksv1.HolderReference) error {
5555
switch obj := obj.(type) {
5656
case *controlplanev1.KubeadmControlPlaneTemplate:
5757
obj.Annotations = map[string]string{"a": "a"}
@@ -222,7 +222,7 @@ func Test_WalkTemplates(t *testing.T) {
222222
response := &runtimehooksv1.GeneratePatchesResponse{}
223223
request := &runtimehooksv1.GeneratePatchesRequest{Variables: tt.globalVariables, Items: tt.requestItems}
224224

225-
WalkTemplates(context.Background(), decoder, request, response, mutatingFunc, tt.options...)
225+
WalkTemplates(context.Background(), decoder, request, response, struct{}{}, mutatingFunc, tt.options...)
226226

227227
g.Expect(response.Status).To(Equal(tt.expectedResponse.Status))
228228
g.Expect(response.Message).To(ContainSubstring(tt.expectedResponse.Message))
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
Copyright 2021 The Kubernetes 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+
// main is the main package for openapi-gen.
18+
package main
19+
20+
import (
21+
"encoding/json"
22+
"fmt"
23+
"os"
24+
"path"
25+
26+
"github.com/pkg/errors"
27+
flag "github.com/spf13/pflag"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/apimachinery/pkg/util/validation/field"
31+
"k8s.io/klog/v2"
32+
"k8s.io/utils/pointer"
33+
"sigs.k8s.io/controller-tools/pkg/crd"
34+
"sigs.k8s.io/controller-tools/pkg/loader"
35+
"sigs.k8s.io/controller-tools/pkg/markers"
36+
37+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
38+
)
39+
40+
var (
41+
paths = flag.String("paths", "", "Paths with the variable types.")
42+
outputFile = flag.String("output-file", "zz_generated.variables.json", "Output file name.")
43+
)
44+
45+
// FIXME: re-evaluate if we should still use openapi-gen in the other case.
46+
func main() {
47+
flag.Parse()
48+
49+
if *paths == "" {
50+
klog.Exit("--paths must be specified")
51+
}
52+
53+
if *outputFile == "" {
54+
klog.Exit("--output-file must be specified")
55+
}
56+
57+
outputFileExt := path.Ext(*outputFile)
58+
if outputFileExt != ".json" {
59+
klog.Exit("--output-file must have 'json' extension")
60+
}
61+
62+
// FIXME:
63+
// * compare clusterv1.JsonSchemaProps vs kubebuilder marker if something is missing
64+
// * example marker
65+
// * cleanup code here
66+
67+
if err := run(*paths, *outputFile); err != nil {
68+
fmt.Println(err)
69+
os.Exit(1)
70+
}
71+
72+
// FIXME: Current state
73+
// * variable go type => apiextensionsv1.CustomResourceDefinition
74+
// * apiextensionsv1.CustomResourceDefinition => clusterv1.JsonSchemaProps
75+
// * Write schema as go structs to a file
76+
// * Validate: existing util (clusterv1.JsonSchemaProps) => validation result
77+
}
78+
79+
func run(paths, outputFile string) error {
80+
crdGen := crd.Generator{}
81+
82+
roots, err := loader.LoadRoots(paths)
83+
if err != nil {
84+
fmt.Println(err)
85+
}
86+
87+
collector := &markers.Collector{
88+
Registry: &markers.Registry{},
89+
}
90+
if err = crdGen.RegisterMarkers(collector.Registry); err != nil {
91+
return err
92+
}
93+
94+
parser := &crd.Parser{
95+
Collector: collector,
96+
Checker: &loader.TypeChecker{
97+
NodeFilters: []loader.NodeFilter{crdGen.CheckFilter()},
98+
},
99+
IgnoreUnexportedFields: true,
100+
AllowDangerousTypes: false,
101+
GenerateEmbeddedObjectMeta: false,
102+
}
103+
104+
crd.AddKnownTypes(parser)
105+
for _, root := range roots {
106+
parser.NeedPackage(root)
107+
}
108+
109+
kubeKinds := []schema.GroupKind{}
110+
for typeIdent := range parser.Types {
111+
// If we need another way to identify "variable structs": look at: crd.FindKubeKinds(parser, metav1Pkg)
112+
if typeIdent.Name == "Variables" {
113+
kubeKinds = append(kubeKinds, schema.GroupKind{
114+
Group: parser.GroupVersions[typeIdent.Package].Group,
115+
Kind: typeIdent.Name,
116+
})
117+
}
118+
}
119+
120+
// For inspiration: parser.NeedCRDFor(groupKind, nil)
121+
var variables []clusterv1.ClusterClassVariable
122+
for _, groupKind := range kubeKinds {
123+
// Get package for the current GroupKind
124+
var packages []*loader.Package
125+
for pkg, gv := range parser.GroupVersions {
126+
if gv.Group != groupKind.Group {
127+
continue
128+
}
129+
packages = append(packages, pkg)
130+
}
131+
132+
var apiExtensionsSchema *apiextensionsv1.JSONSchemaProps
133+
for _, pkg := range packages {
134+
typeIdent := crd.TypeIdent{Package: pkg, Name: groupKind.Kind}
135+
typeInfo := parser.Types[typeIdent]
136+
137+
// Didn't find type in pkg.
138+
if typeInfo == nil {
139+
continue
140+
}
141+
142+
parser.NeedFlattenedSchemaFor(typeIdent)
143+
fullSchema := parser.FlattenedSchemata[typeIdent]
144+
apiExtensionsSchema = fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc)
145+
}
146+
147+
if apiExtensionsSchema == nil {
148+
return errors.Errorf("Couldn't find schema for %s", groupKind)
149+
}
150+
151+
for variableName, variableSchema := range apiExtensionsSchema.Properties {
152+
vs := variableSchema
153+
openAPIV3Schema, errs := convertToJSONSchemaProps(&vs, field.NewPath("schema"))
154+
if len(errs) > 0 {
155+
return errs.ToAggregate()
156+
}
157+
158+
variable := clusterv1.ClusterClassVariable{
159+
Name: variableName,
160+
Schema: clusterv1.VariableSchema{
161+
OpenAPIV3Schema: *openAPIV3Schema,
162+
},
163+
}
164+
165+
for _, requiredVariable := range apiExtensionsSchema.Required {
166+
if variableName == requiredVariable {
167+
variable.Required = true
168+
}
169+
}
170+
171+
variables = append(variables, variable)
172+
}
173+
}
174+
175+
res, err := json.MarshalIndent(variables, "", " ")
176+
if err != nil {
177+
return err
178+
}
179+
180+
if err := os.WriteFile(outputFile, res, 0600); err != nil {
181+
return errors.Wrapf(err, "failed to write generated file")
182+
}
183+
184+
return nil
185+
}
186+
187+
// JSONSchemaProps converts a apiextensions.JSONSchemaProp to a clusterv1.JSONSchemaProps.
188+
func convertToJSONSchemaProps(schema *apiextensionsv1.JSONSchemaProps, fldPath *field.Path) (*clusterv1.JSONSchemaProps, field.ErrorList) {
189+
var allErrs field.ErrorList
190+
191+
props := &clusterv1.JSONSchemaProps{
192+
Description: schema.Description,
193+
Type: schema.Type,
194+
Required: schema.Required,
195+
MaxItems: schema.MaxItems,
196+
MinItems: schema.MinItems,
197+
UniqueItems: schema.UniqueItems,
198+
Format: schema.Format,
199+
MaxLength: schema.MaxLength,
200+
MinLength: schema.MinLength,
201+
Pattern: schema.Pattern,
202+
ExclusiveMaximum: schema.ExclusiveMaximum,
203+
ExclusiveMinimum: schema.ExclusiveMinimum,
204+
Default: schema.Default,
205+
Enum: schema.Enum,
206+
Example: schema.Example,
207+
XPreserveUnknownFields: pointer.BoolDeref(schema.XPreserveUnknownFields, false),
208+
}
209+
210+
if schema.Maximum != nil {
211+
f := int64(*schema.Maximum)
212+
props.Maximum = &f
213+
}
214+
215+
if schema.Minimum != nil {
216+
f := int64(*schema.Minimum)
217+
props.Minimum = &f
218+
}
219+
220+
if schema.AdditionalProperties != nil {
221+
jsonSchemaProps, err := convertToJSONSchemaProps(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"))
222+
if err != nil {
223+
allErrs = append(allErrs, field.Invalid(fldPath.Child("additionalProperties"), "",
224+
fmt.Sprintf("failed to convert schema: %v", err)))
225+
} else {
226+
props.AdditionalProperties = jsonSchemaProps
227+
}
228+
}
229+
230+
if len(schema.Properties) > 0 {
231+
props.Properties = map[string]clusterv1.JSONSchemaProps{}
232+
for propertyName, propertySchema := range schema.Properties {
233+
p := propertySchema
234+
jsonSchemaProps, err := convertToJSONSchemaProps(&p, fldPath.Child("properties").Key(propertyName))
235+
if err != nil {
236+
allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key(propertyName), "",
237+
fmt.Sprintf("failed to convert schema: %v", err)))
238+
} else {
239+
props.Properties[propertyName] = *jsonSchemaProps
240+
}
241+
}
242+
}
243+
244+
if schema.Items != nil {
245+
jsonSchemaProps, err := convertToJSONSchemaProps(schema.Items.Schema, fldPath.Child("items"))
246+
if err != nil {
247+
allErrs = append(allErrs, field.Invalid(fldPath.Child("items"), "",
248+
fmt.Sprintf("failed to convert schema: %v", err)))
249+
} else {
250+
props.Items = jsonSchemaProps
251+
}
252+
}
253+
254+
return props, allErrs
255+
}

test/extension/api/doc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2023 The Kubernetes 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 api contains the API of the Runtime Extension which is consists of its variable definitions
18+
// used for topology mutation hooks.
19+
// By writing variables as Go types the resulting code is cleaner, typesafe and easily testable.
20+
// +groupName=variables.cluster.x-k8s.io
21+
package api

0 commit comments

Comments
 (0)