github.com/crossplane/upjet@v1.3.0/pkg/examples/example.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package examples
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"strings"
    16  
    17  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    18  	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
    19  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    20  	"github.com/pkg/errors"
    21  	"sigs.k8s.io/yaml"
    22  
    23  	"github.com/crossplane/upjet/pkg/config"
    24  	"github.com/crossplane/upjet/pkg/registry/reference"
    25  	"github.com/crossplane/upjet/pkg/resource/json"
    26  	tjtypes "github.com/crossplane/upjet/pkg/types"
    27  	"github.com/crossplane/upjet/pkg/types/name"
    28  )
    29  
    30  var (
    31  	reFile = regexp.MustCompile(`file\("(.+)"\)`)
    32  )
    33  
    34  const (
    35  	labelExampleName       = "testing.upbound.io/example-name"
    36  	annotationExampleGroup = "meta.upbound.io/example-id"
    37  	defaultExampleName     = "example"
    38  	defaultNamespace       = "upbound-system"
    39  )
    40  
    41  // Generator represents a pipeline for generating example manifests.
    42  // Generates example manifests for Terraform resources under examples-generated.
    43  type Generator struct {
    44  	reference.Injector
    45  	rootDir         string
    46  	configResources map[string]*config.Resource
    47  	resources       map[string]*reference.PavedWithManifest
    48  }
    49  
    50  // NewGenerator returns a configured Generator
    51  func NewGenerator(rootDir, modulePath, shortName string, configResources map[string]*config.Resource) *Generator {
    52  	return &Generator{
    53  		Injector: reference.Injector{
    54  			ModulePath:        modulePath,
    55  			ProviderShortName: shortName,
    56  		},
    57  		rootDir:         rootDir,
    58  		configResources: configResources,
    59  		resources:       make(map[string]*reference.PavedWithManifest),
    60  	}
    61  }
    62  
    63  // StoreExamples stores the generated example manifests under examples-generated in
    64  // their respective API groups.
    65  func (eg *Generator) StoreExamples() error { // nolint:gocyclo
    66  	for rn, pm := range eg.resources {
    67  		manifestDir := filepath.Dir(pm.ManifestPath)
    68  		if err := os.MkdirAll(manifestDir, 0750); err != nil {
    69  			return errors.Wrapf(err, "cannot mkdir %s", manifestDir)
    70  		}
    71  		var buff bytes.Buffer
    72  		if err := eg.writeManifest(&buff, pm, &reference.ResolutionContext{
    73  			WildcardNames: true,
    74  			Context:       eg.resources,
    75  		}); err != nil {
    76  			return errors.Wrapf(err, "cannot store example manifest for resource: %s", rn)
    77  		}
    78  		if r, ok := eg.configResources[reference.NewRefPartsFromResourceName(rn).Resource]; ok && r.MetaResource != nil {
    79  			re := r.MetaResource.Examples[0]
    80  			context, err := reference.PrepareLocalResolutionContext(re, reference.NewRefParts(reference.NewRefPartsFromResourceName(rn).Resource, re.Name).GetResourceName(false))
    81  			if err != nil {
    82  				return errors.Wrapf(err, "cannot prepare local resolution context for resource: %s", rn)
    83  			}
    84  			dKeys := make([]string, 0, len(re.Dependencies))
    85  			for k := range re.Dependencies {
    86  				dKeys = append(dKeys, k)
    87  			}
    88  			sort.Strings(dKeys)
    89  			for _, dn := range dKeys {
    90  				dr, ok := eg.resources[reference.NewRefPartsFromResourceName(dn).GetResourceName(true)]
    91  				if !ok {
    92  					continue
    93  				}
    94  				var exampleParams map[string]any
    95  				if err := json.TFParser.Unmarshal([]byte(re.Dependencies[dn]), &exampleParams); err != nil {
    96  					return errors.Wrapf(err, "cannot unmarshal example manifest for resource: %s", dr.Config.Name)
    97  				}
    98  				// e.g. meta.upbound.io/example-id: ec2/v1beta1/instance
    99  				eGroup := fmt.Sprintf("%s/%s/%s", strings.ToLower(r.ShortGroup), r.Version, strings.ToLower(r.Kind))
   100  				pmd := paveCRManifest(exampleParams, dr.Config,
   101  					reference.NewRefPartsFromResourceName(dn).ExampleName, dr.Group, dr.Version, eGroup)
   102  				if err := eg.writeManifest(&buff, pmd, context); err != nil {
   103  					return errors.Wrapf(err, "cannot store example manifest for %s dependency: %s", rn, dn)
   104  				}
   105  			}
   106  		}
   107  
   108  		newBuff := bytes.TrimSuffix(buff.Bytes(), []byte("\n---\n\n"))
   109  
   110  		// no sensitive info in the example manifest
   111  		if err := os.WriteFile(pm.ManifestPath, newBuff, 0600); err != nil {
   112  			return errors.Wrapf(err, "cannot write example manifest file %s for resource %s", pm.ManifestPath, rn)
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  func paveCRManifest(exampleParams map[string]any, r *config.Resource, eName, group, version, eGroup string) *reference.PavedWithManifest {
   119  	delete(exampleParams, "depends_on")
   120  	delete(exampleParams, "lifecycle")
   121  	transformFields(r, exampleParams, r.ExternalName.OmittedFields, "")
   122  	metadata := map[string]any{
   123  		"labels": map[string]string{
   124  			labelExampleName: eName,
   125  		},
   126  		"annotations": map[string]string{
   127  			annotationExampleGroup: eGroup,
   128  		},
   129  	}
   130  	example := map[string]any{
   131  		"apiVersion": fmt.Sprintf("%s/%s", group, version),
   132  		"kind":       r.Kind,
   133  		"metadata":   metadata,
   134  		"spec": map[string]any{
   135  			"forProvider": exampleParams,
   136  		},
   137  	}
   138  	if len(r.MetaResource.ExternalName) != 0 {
   139  		metadata["annotations"].(map[string]string)[xpmeta.AnnotationKeyExternalName] = r.MetaResource.ExternalName
   140  	}
   141  	return &reference.PavedWithManifest{
   142  		Paved:        fieldpath.Pave(example),
   143  		ParamsPrefix: []string{"spec", "forProvider"},
   144  		Config:       r,
   145  		Group:        group,
   146  		Version:      version,
   147  	}
   148  }
   149  
   150  func dns1123Name(name string) string {
   151  	return strings.ReplaceAll(strings.ToLower(name), "_", "-")
   152  }
   153  
   154  func (eg *Generator) writeManifest(writer io.Writer, pm *reference.PavedWithManifest, resolutionContext *reference.ResolutionContext) error {
   155  	if err := eg.ResolveReferencesOfPaved(pm, resolutionContext); err != nil {
   156  		return errors.Wrap(err, "cannot resolve references of resource")
   157  	}
   158  	labels, err := pm.Paved.GetValue("metadata.labels")
   159  	if err != nil {
   160  		return errors.Wrap(err, `cannot get "metadata.labels" from paved`)
   161  	}
   162  	pm.ExampleName = dns1123Name(labels.(map[string]string)[labelExampleName])
   163  	if err := pm.Paved.SetValue("metadata.name", pm.ExampleName); err != nil {
   164  		return errors.Wrapf(err, `cannot set "metadata.name" for resource %q:%s`, pm.Config.Name, pm.ExampleName)
   165  	}
   166  	u := pm.Paved.UnstructuredContent()
   167  	buff, err := yaml.Marshal(u)
   168  	if err != nil {
   169  		return errors.Wrap(err, "cannot marshal example resource manifest")
   170  	}
   171  	if _, err := writer.Write(buff); err != nil {
   172  		return errors.Wrap(err, "cannot write resource manifest to the underlying stream")
   173  	}
   174  	_, err = writer.Write([]byte("\n---\n\n"))
   175  	return errors.Wrap(err, "cannot write YAML document separator to the underlying stream")
   176  }
   177  
   178  // Generate generates an example manifest for the specified Terraform resource.
   179  func (eg *Generator) Generate(group, version string, r *config.Resource) error {
   180  	rm := eg.configResources[r.Name].MetaResource
   181  	if rm == nil || len(rm.Examples) == 0 {
   182  		return nil
   183  	}
   184  	groupPrefix := strings.ToLower(strings.Split(group, ".")[0])
   185  	// e.g. gvk = ec2/v1beta1/instance
   186  	gvk := fmt.Sprintf("%s/%s/%s", groupPrefix, version, strings.ToLower(r.Kind))
   187  	pm := paveCRManifest(rm.Examples[0].Paved.UnstructuredContent(), r, rm.Examples[0].Name, group, version, gvk)
   188  	manifestDir := filepath.Join(eg.rootDir, "examples-generated", groupPrefix, r.Version)
   189  	pm.ManifestPath = filepath.Join(manifestDir, fmt.Sprintf("%s.yaml", strings.ToLower(r.Kind)))
   190  	eg.resources[fmt.Sprintf("%s.%s", r.Name, reference.Wildcard)] = pm
   191  	return nil
   192  }
   193  
   194  func getHierarchicalName(prefix, name string) string {
   195  	if prefix == "" {
   196  		return name
   197  	}
   198  	return fmt.Sprintf("%s.%s", prefix, name)
   199  }
   200  
   201  func isStatus(r *config.Resource, attr string) bool {
   202  	s := config.GetSchema(r.TerraformResource, attr)
   203  	if s == nil {
   204  		return false
   205  	}
   206  	return tjtypes.IsObservation(s)
   207  }
   208  
   209  func transformFields(r *config.Resource, params map[string]any, omittedFields []string, namePrefix string) { // nolint:gocyclo
   210  	for n := range params {
   211  		hName := getHierarchicalName(namePrefix, n)
   212  		if isStatus(r, hName) {
   213  			delete(params, n)
   214  			continue
   215  		}
   216  		for _, hn := range omittedFields {
   217  			if hn == hName {
   218  				delete(params, n)
   219  				break
   220  			}
   221  		}
   222  	}
   223  
   224  	for n, v := range params {
   225  		switch pT := v.(type) {
   226  		case map[string]any:
   227  			transformFields(r, pT, omittedFields, getHierarchicalName(namePrefix, n))
   228  
   229  		case []any:
   230  			for _, e := range pT {
   231  				eM, ok := e.(map[string]any)
   232  				if !ok {
   233  					continue
   234  				}
   235  				transformFields(r, eM, omittedFields, getHierarchicalName(namePrefix, n))
   236  			}
   237  		}
   238  	}
   239  
   240  	for n, v := range params {
   241  		fieldPath := getHierarchicalName(namePrefix, n)
   242  		sch := config.GetSchema(r.TerraformResource, fieldPath)
   243  		if sch == nil {
   244  			continue
   245  		}
   246  		// At this point, we confirmed that the field is part of the schema,
   247  		// so we'll need to perform at least name change on it.
   248  		delete(params, n)
   249  		fn := name.NewFromSnake(n)
   250  		switch {
   251  		case sch.Sensitive:
   252  			secretName, secretKey := getSecretRef(v)
   253  			params[fn.LowerCamelComputed+"SecretRef"] = getRefField(v, map[string]any{
   254  				"name":      secretName,
   255  				"namespace": defaultNamespace,
   256  				"key":       secretKey,
   257  			})
   258  		case r.References[fieldPath] != config.Reference{}:
   259  			switch v.(type) {
   260  			case []any:
   261  				l := sch.Type == schema.TypeList || sch.Type == schema.TypeSet
   262  				ref := name.ReferenceFieldName(fn, l, r.References[fieldPath].RefFieldName)
   263  				params[ref.LowerCamelComputed] = getNameRefField(v)
   264  			default:
   265  				sel := name.SelectorFieldName(fn, r.References[fieldPath].SelectorFieldName)
   266  				params[sel.LowerCamelComputed] = getSelectorField(v)
   267  			}
   268  		default:
   269  			params[fn.LowerCamelComputed] = v
   270  		}
   271  	}
   272  }
   273  
   274  func getNameRefField(v any) any {
   275  	arr := v.([]any)
   276  	refArr := make([]map[string]any, len(arr))
   277  	for i, r := range arr {
   278  		refArr[i] = map[string]any{
   279  			"name": defaultExampleName,
   280  		}
   281  		if parts := reference.MatchRefParts(fmt.Sprintf("%v", r)); parts != nil {
   282  			refArr[i]["name"] = parts.ExampleName
   283  		}
   284  	}
   285  	return refArr
   286  }
   287  
   288  func getSelectorField(refVal any) any {
   289  	ref := map[string]string{
   290  		labelExampleName: defaultExampleName,
   291  	}
   292  	if parts := reference.MatchRefParts(fmt.Sprintf("%v", refVal)); parts != nil {
   293  		ref[labelExampleName] = parts.ExampleName
   294  	}
   295  	return map[string]any{
   296  		"matchLabels": ref,
   297  	}
   298  }
   299  
   300  func getRefField(v any, ref map[string]any) any {
   301  	switch v.(type) {
   302  	case []any:
   303  		return []any{
   304  			ref,
   305  		}
   306  
   307  	default:
   308  		return ref
   309  	}
   310  }
   311  
   312  func getSecretRef(v any) (string, string) {
   313  	secretName := "example-secret"
   314  	secretKey := "example-key"
   315  	s, ok := v.(string)
   316  	if !ok {
   317  		return secretName, secretKey
   318  	}
   319  	g := reference.ReRef.FindStringSubmatch(s)
   320  	if len(g) != 2 {
   321  		return secretName, secretKey
   322  	}
   323  	f := reFile.FindStringSubmatch(g[1])
   324  	switch {
   325  	case len(f) == 2: // then a file reference
   326  		_, file := filepath.Split(f[1])
   327  		secretKey = fmt.Sprintf("attribute.%s", file)
   328  	default:
   329  		parts := strings.Split(g[1], ".")
   330  		if len(parts) < 3 {
   331  			return secretName, secretKey
   332  		}
   333  		secretName = fmt.Sprintf("example-%s", strings.Join(strings.Split(parts[0], "_")[1:], "-"))
   334  		secretKey = fmt.Sprintf("attribute.%s", strings.Join(parts[2:], "."))
   335  	}
   336  	return secretName, secretKey
   337  }