Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
sbueringer committed Jan 30, 2024
1 parent 4cbc679 commit 316a012
Show file tree
Hide file tree
Showing 10 changed files with 581 additions and 186 deletions.
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ providers = {
# Add the ExtensionConfig for this Runtime extension; given that the ExtensionConfig can be installed only when capi_controller
# are up and running, it is required to set a resource_deps to ensure proper install order.
"additional_resources": [
"config/tilt/extensionconfig.yaml",
"tilt/extensionconfig.yaml",
],
"resource_deps": ["capi_controller"],
},
Expand Down
37 changes: 34 additions & 3 deletions exp/runtime/topologymutation/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package topologymutation
import (
"context"
"encoding/json"
"fmt"

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

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

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

// FIXME: let's do this for all variables before we actually call `mutateFunc`
// FIXME: convert variable values to go types
// godoc, handle errors, ...
variableValuesJSON := map[string]apiextensionsv1.JSON{}
var builtinVariableJSON apiextensionsv1.JSON
for variableName, variableValue := range templateVariables {
if variableName == patchvariables.BuiltinsName {
builtinVariableJSON = variableValue
continue
}

variableValuesJSON[variableName] = variableValue
}

variableValuesJSONAll, err := json.Marshal(variableValuesJSON)
if err != nil {
fmt.Println(err)
}

var builtinVariableValue patchvariables.Builtins
if err := json.Unmarshal(builtinVariableJSON.Raw, &builtinVariableValue); err != nil {
fmt.Println(err)
}

// FIXME: just using variablesType directly is probably not clean enough
if err := json.Unmarshal(variableValuesJSONAll, &variablesType); err != nil {
fmt.Println(err)
}

// Convert the template object into a typed object.
original, _, err := decoder.Decode(requestItem.Object.Raw, nil, requestItem.Object.Object)
if err != nil {
Expand Down Expand Up @@ -138,7 +169,7 @@ func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehoo
// Calls the mutateFunc.
requestItemLog.V(4).Info("Generating patch for template")
modified := original.DeepCopyObject()
if err := mutateFunc(requestItemCtx, modified, templateVariables, requestItem.HolderReference); err != nil {
if err := mutateFunc(requestItemCtx, modified, &builtinVariableValue, variablesType, requestItem.HolderReference); err != nil {
resp.Status = runtimehooksv1.ResponseStatusFailure
resp.Message = err.Error()
return
Expand Down
4 changes: 2 additions & 2 deletions exp/runtime/topologymutation/walker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func Test_WalkTemplates(t *testing.T) {
controlplanev1.GroupVersion,
bootstrapv1.GroupVersion,
)
mutatingFunc := func(ctx context.Context, obj runtime.Object, variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error {
mutatingFunc := func(ctx context.Context, obj runtime.Object, builtinVariable *variables.Builtins, variables interface{}, holderRef runtimehooksv1.HolderReference) error {
switch obj := obj.(type) {
case *controlplanev1.KubeadmControlPlaneTemplate:
obj.Annotations = map[string]string{"a": "a"}
Expand Down Expand Up @@ -222,7 +222,7 @@ func Test_WalkTemplates(t *testing.T) {
response := &runtimehooksv1.GeneratePatchesResponse{}
request := &runtimehooksv1.GeneratePatchesRequest{Variables: tt.globalVariables, Items: tt.requestItems}

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

g.Expect(response.Status).To(Equal(tt.expectedResponse.Status))
g.Expect(response.Message).To(ContainSubstring(tt.expectedResponse.Message))
Expand Down
255 changes: 255 additions & 0 deletions hack/tools/runtime-openapi-gen-2/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// main is the main package for openapi-gen.
package main

import (
"encoding/json"
"fmt"
"os"
"path"

"github.com/pkg/errors"
flag "github.com/spf13/pflag"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-tools/pkg/crd"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)

var (
paths = flag.String("paths", "", "Paths with the variable types.")
outputFile = flag.String("output-file", "zz_generated.variables.json", "Output file name.")
)

// FIXME: re-evaluate if we should still use openapi-gen in the other case.
func main() {
flag.Parse()

if *paths == "" {
klog.Exit("--paths must be specified")
}

if *outputFile == "" {
klog.Exit("--output-file must be specified")
}

outputFileExt := path.Ext(*outputFile)
if outputFileExt != ".json" {
klog.Exit("--output-file must have 'json' extension")
}

// FIXME:
// * compare clusterv1.JsonSchemaProps vs kubebuilder marker if something is missing
// * example marker
// * cleanup code here

if err := run(*paths, *outputFile); err != nil {
fmt.Println(err)
os.Exit(1)
}

// FIXME: Current state
// * variable go type => apiextensionsv1.CustomResourceDefinition
// * apiextensionsv1.CustomResourceDefinition => clusterv1.JsonSchemaProps
// * Write schema as go structs to a file
// * Validate: existing util (clusterv1.JsonSchemaProps) => validation result
}

func run(paths, outputFile string) error {
crdGen := crd.Generator{}

roots, err := loader.LoadRoots(paths)
if err != nil {
fmt.Println(err)
}

collector := &markers.Collector{
Registry: &markers.Registry{},
}
if err = crdGen.RegisterMarkers(collector.Registry); err != nil {
return err
}

parser := &crd.Parser{
Collector: collector,
Checker: &loader.TypeChecker{
NodeFilters: []loader.NodeFilter{crdGen.CheckFilter()},
},
IgnoreUnexportedFields: true,
AllowDangerousTypes: false,
GenerateEmbeddedObjectMeta: false,
}

crd.AddKnownTypes(parser)
for _, root := range roots {
parser.NeedPackage(root)
}

kubeKinds := []schema.GroupKind{}
for typeIdent := range parser.Types {
// If we need another way to identify "variable structs": look at: crd.FindKubeKinds(parser, metav1Pkg)
if typeIdent.Name == "Variables" {
kubeKinds = append(kubeKinds, schema.GroupKind{
Group: parser.GroupVersions[typeIdent.Package].Group,
Kind: typeIdent.Name,
})
}
}

// For inspiration: parser.NeedCRDFor(groupKind, nil)
var variables []clusterv1.ClusterClassVariable
for _, groupKind := range kubeKinds {
// Get package for the current GroupKind
var packages []*loader.Package
for pkg, gv := range parser.GroupVersions {
if gv.Group != groupKind.Group {
continue
}
packages = append(packages, pkg)
}

var apiExtensionsSchema *apiextensionsv1.JSONSchemaProps
for _, pkg := range packages {
typeIdent := crd.TypeIdent{Package: pkg, Name: groupKind.Kind}
typeInfo := parser.Types[typeIdent]

// Didn't find type in pkg.
if typeInfo == nil {
continue
}

parser.NeedFlattenedSchemaFor(typeIdent)
fullSchema := parser.FlattenedSchemata[typeIdent]
apiExtensionsSchema = fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc)
}

if apiExtensionsSchema == nil {
return errors.Errorf("Couldn't find schema for %s", groupKind)
}

for variableName, variableSchema := range apiExtensionsSchema.Properties {
vs := variableSchema
openAPIV3Schema, errs := convertToJSONSchemaProps(&vs, field.NewPath("schema"))
if len(errs) > 0 {
return errs.ToAggregate()
}

variable := clusterv1.ClusterClassVariable{
Name: variableName,
Schema: clusterv1.VariableSchema{
OpenAPIV3Schema: *openAPIV3Schema,
},
}

for _, requiredVariable := range apiExtensionsSchema.Required {
if variableName == requiredVariable {
variable.Required = true
}
}

variables = append(variables, variable)
}
}

res, err := json.MarshalIndent(variables, "", " ")
if err != nil {
return err
}

if err := os.WriteFile(outputFile, res, 0600); err != nil {
return errors.Wrapf(err, "failed to write generated file")
}

return nil
}

// JSONSchemaProps converts a apiextensions.JSONSchemaProp to a clusterv1.JSONSchemaProps.
func convertToJSONSchemaProps(schema *apiextensionsv1.JSONSchemaProps, fldPath *field.Path) (*clusterv1.JSONSchemaProps, field.ErrorList) {
var allErrs field.ErrorList

props := &clusterv1.JSONSchemaProps{
Description: schema.Description,
Type: schema.Type,
Required: schema.Required,
MaxItems: schema.MaxItems,
MinItems: schema.MinItems,
UniqueItems: schema.UniqueItems,
Format: schema.Format,
MaxLength: schema.MaxLength,
MinLength: schema.MinLength,
Pattern: schema.Pattern,
ExclusiveMaximum: schema.ExclusiveMaximum,
ExclusiveMinimum: schema.ExclusiveMinimum,
Default: schema.Default,
Enum: schema.Enum,
Example: schema.Example,
XPreserveUnknownFields: ptr.Deref(schema.XPreserveUnknownFields, false),
}

if schema.Maximum != nil {
f := int64(*schema.Maximum)
props.Maximum = &f
}

if schema.Minimum != nil {
f := int64(*schema.Minimum)
props.Minimum = &f
}

if schema.AdditionalProperties != nil {
jsonSchemaProps, err := convertToJSONSchemaProps(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"))
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("additionalProperties"), "",
fmt.Sprintf("failed to convert schema: %v", err)))
} else {
props.AdditionalProperties = jsonSchemaProps
}
}

if len(schema.Properties) > 0 {
props.Properties = map[string]clusterv1.JSONSchemaProps{}
for propertyName, propertySchema := range schema.Properties {
p := propertySchema
jsonSchemaProps, err := convertToJSONSchemaProps(&p, fldPath.Child("properties").Key(propertyName))
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("properties").Key(propertyName), "",
fmt.Sprintf("failed to convert schema: %v", err)))
} else {
props.Properties[propertyName] = *jsonSchemaProps
}
}
}

if schema.Items != nil {
jsonSchemaProps, err := convertToJSONSchemaProps(schema.Items.Schema, fldPath.Child("items"))
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("items"), "",
fmt.Sprintf("failed to convert schema: %v", err)))
} else {
props.Items = jsonSchemaProps
}
}

return props, allErrs
}
21 changes: 21 additions & 0 deletions test/extension/api/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package api contains the API of the Runtime Extension which is consists of its variable definitions
// used for topology mutation hooks.
// By writing variables as Go types the resulting code is cleaner, typesafe and easily testable.
// +groupName=variables.cluster.x-k8s.io
package api
Loading

0 comments on commit 316a012

Please sign in to comment.