Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 126 additions & 14 deletions pkg/examples/conversion/example_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
"log"
"os"
"path/filepath"
"sort"
"strings"

"github.com/crossplane/crossplane-runtime/v2/pkg/fieldpath"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
Expand All @@ -21,8 +23,100 @@ import (

"github.com/crossplane/upjet/v2/pkg/config"
"github.com/crossplane/upjet/v2/pkg/config/conversion"
"github.com/crossplane/upjet/v2/pkg/schema/traverser"
"github.com/crossplane/upjet/v2/pkg/types/conversion/tfjson"
)

// schemaTypeObjectCollector is a schema traverser that collects the CRD field
// paths for SchemaTypeObject (NestingModeSingle) fields. These fields are
// represented as embedded objects (pointer-to-struct) in the CRD but may appear
// as single-element arrays in scraped Terraform example configurations.
type schemaTypeObjectCollector struct {
traverser.NoopTraverser
paths []string
}

func (c *schemaTypeObjectCollector) VisitResource(r *traverser.ResourceNode) error {
if r.Schema.Type == tfjson.SchemaTypeObject {
c.paths = append(c.paths, traverser.FieldPathWithWildcard(r.CRDPath))
}
return nil
}

// collectSchemaTypeObjectCRDPaths returns the CRD field paths for all
// SchemaTypeObject fields in the given resource's Terraform schema.
func collectSchemaTypeObjectCRDPaths(r *config.Resource) ([]string, error) {
collector := &schemaTypeObjectCollector{}
if err := traverser.Traverse(r.Name, r.TerraformResource, collector); err != nil {
return nil, errors.Wrapf(err, "failed to traverse the schema of resource %s", r.Name)
}
return collector.paths, nil
}

// flattenSchemaTypeObjectExamples flattens single-element arrays at the given
// field paths to plain objects in the provided unstructured object.
// Unlike conversion.Convert, this function is lenient: it silently skips paths
// that don't exist or whose values are not single-element arrays.
// This is necessary because conversion.Convert returns an error for non-slice
// values, which is correct for runtime conversions but too strict for example
// manifests where the value may not be present or may already be an object.
func flattenSchemaTypeObjectExamples(obj map[string]any, paths []string) {
if len(paths) == 0 {
return
}
// Sort in lexical order so parents are flattened before children.
// This is necessary because SchemaTypeObject paths do NOT use [*]
// wildcards (unlike TypeList/TypeSet paths). Without wildcards,
// child paths like "outer.inner" can't be resolved while "outer" is
// still an array. Flattening parents first converts them to objects,
// making child paths accessible.
sortedPaths := append([]string(nil), paths...)
sort.Strings(sortedPaths)
pv := fieldpath.Pave(obj)
for _, fp := range sortedPaths {
exp, err := pv.ExpandWildcards(fp)
if err != nil {
// Path doesn't exist in the object, skip.
continue
}
for _, e := range exp {
flattenSingleElementArray(pv, e)
}
}
}

// flattenSingleElementArray replaces a single-element array at the given
// concrete field path with its sole element. It is a no-op if the value
// does not exist, is not a []any, or does not have exactly one element.
func flattenSingleElementArray(pv *fieldpath.Paved, path string) {
v, err := pv.GetValue(path)
if err != nil {
return
}
s, ok := v.([]any)
if !ok || len(s) != 1 {
return
}
// Set the value to the single element by accessing the parent map
// directly. This avoids type coercion that fieldpath.Paved.SetValue
// might perform.
segments := strings.Split(path, ".")
key := segments[len(segments)-1]
var parentVal any = pv.UnstructuredContent()
if len(segments) > 1 {
parentPath := strings.Join(segments[:len(segments)-1], ".")
parentVal, err = pv.GetValue(parentPath)
if err != nil {
return
}
}
parent, ok := parentVal.(map[string]any)
if !ok {
return
}
parent[key] = s[0]
}

// ApplyAPIConverters applies the registered converters to generated
// example manifests under the given root directory.
// All (generated) manifests under the `startPath` are scanned and the
Expand Down Expand Up @@ -67,23 +161,41 @@ func ApplyAPIConverters(pc *config.Provider, startPath, licenseHeaderPath string
annotationValue := strings.ToLower(fmt.Sprintf("%s/%s/%s", rootResource.ShortGroup, rootResource.Version, rootResource.Kind))
for _, e := range examples {
if resource, ok := resourceRegistry[fmt.Sprintf("%s/%s", e.GroupVersionKind().Kind, e.GroupVersionKind().Group)]; ok {
conversionPaths := resource.CRDListConversionPaths()
// if the latest version has conversions, run the conversions on the
// example manifest.
// Please note that only the version being generated (latest version)
// is processed.
if conversionPaths != nil && e.GroupVersionKind().Version == resource.Version {
for i, cp := range conversionPaths {
// Here, for the manifests to be converted, only `forProvider
// is converted, assuming the `initProvider` field is empty in the
// spec.
conversionPaths[i] = "spec.forProvider." + cp
if e.GroupVersionKind().Version == resource.Version {
conversionPaths := resource.CRDListConversionPaths()
// if the latest version has conversions, run the conversions on the
// example manifest.
// Please note that only the version being generated (latest version)
// is processed.
if conversionPaths != nil {
for i, cp := range conversionPaths {
// Here, for the manifests to be converted, only `forProvider
// is converted, assuming the `initProvider` field is empty in the
// spec.
conversionPaths[i] = "spec.forProvider." + cp
}
converted, err := conversion.Convert(e.Object, conversionPaths, conversion.ToEmbeddedObject, nil)
if err != nil {
return errors.Wrapf(err, "failed to convert example to embedded object in manifest %s", path)
}
e.Object = converted
}
converted, err := conversion.Convert(e.Object, conversionPaths, conversion.ToEmbeddedObject, nil)

// Also flatten SchemaTypeObject (NestingModeSingle) fields.
// These fields are generated as embedded objects in the CRD
// but the HCL-to-JSON scraper wraps them in single-element
// arrays per HCL spec.
objectPaths, err := collectSchemaTypeObjectCRDPaths(resource)
if err != nil {
return errors.Wrapf(err, "failed to convert example to embedded object in manifest %s", path)
return errors.Wrapf(err, "failed to collect SchemaTypeObject paths for resource %s", resource.Name)
}
if len(objectPaths) > 0 {
for i, op := range objectPaths {
objectPaths[i] = "spec.forProvider." + op
}
flattenSchemaTypeObjectExamples(e.Object, objectPaths)
}
e.Object = converted

e.SetGroupVersionKind(k8sschema.GroupVersionKind{
Group: e.GroupVersionKind().Group,
Version: resource.Version,
Expand Down
Loading
Loading