Skip to content

Commit

Permalink
Merge pull request #4 from ulucinar/expipeline
Browse files Browse the repository at this point in the history
Add example manifest generation pipeline
  • Loading branch information
ulucinar authored Jun 9, 2022
2 parents 39f5696 + 00b7d18 commit 1a4a079
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 38 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
k8s.io/apimachinery v0.24.0
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
sigs.k8s.io/controller-runtime v0.12.1
sigs.k8s.io/yaml v1.3.0
)

require (
Expand Down Expand Up @@ -127,5 +128,4 @@ require (
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
21 changes: 12 additions & 9 deletions pkg/config/provider.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
Copyright 2022 Upbound Inc.
*/

package config

import (
Expand Down Expand Up @@ -96,6 +100,10 @@ type Provider struct {
// resource name.
Resources map[string]*Resource

// ProviderMetadataPath is the scraped provider metadata file path
// from Terraform registry
ProviderMetadataPath string

// resourceConfigurators is a map holding resource configurators where key
// is Terraform resource name.
resourceConfigurators map[string]ResourceConfiguratorChain
Expand Down Expand Up @@ -146,10 +154,10 @@ func WithDefaultResourceFn(f DefaultResourceFn) ProviderOption {
}
}

// NewProviderWithSchema builds and returns a new Provider from provider
// NewProvider builds and returns a new Provider from provider
// tfjson schema, that is generated using Terraform CLI with:
// `terraform providers schema --json`
func NewProviderWithSchema(schema []byte, prefix string, modulePath string, opts ...ProviderOption) *Provider {
func NewProvider(schema []byte, prefix string, modulePath string, metadataPath string, opts ...ProviderOption) *Provider {
ps := tfjson.ProviderSchemas{}
if err := ps.UnmarshalJSON(schema); err != nil {
panic(err)
Expand All @@ -162,13 +170,8 @@ func NewProviderWithSchema(schema []byte, prefix string, modulePath string, opts
rs = v.ResourceSchemas
break
}
return NewProvider(conversiontfjson.GetV2ResourceMap(rs), prefix, modulePath, opts...)
}

// NewProvider builds and returns a new Provider.
// Deprecated: This function will be removed soon, please use
// NewProviderWithSchema instead.
func NewProvider(resourceMap map[string]*schema.Resource, prefix string, modulePath string, opts ...ProviderOption) *Provider {
resourceMap := conversiontfjson.GetV2ResourceMap(rs)
p := &Provider{
ModulePath: modulePath,
TerraformResourcePrefix: fmt.Sprintf("%s_", prefix),
Expand All @@ -181,6 +184,7 @@ func NewProvider(resourceMap map[string]*schema.Resource, prefix string, moduleP
".+",
},
Resources: map[string]*Resource{},
ProviderMetadataPath: metadataPath,
resourceConfigurators: map[string]ResourceConfiguratorChain{},
}

Expand All @@ -204,7 +208,6 @@ func NewProvider(resourceMap map[string]*schema.Resource, prefix string, moduleP

p.Resources[name] = p.DefaultResourceFn(name, terraformResource)
}

return p
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/pipeline/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type CRDGenerator struct {
Group string
ProviderShortName string
LicenseHeaderPath string
Generated *tjtypes.Generated

pkg *types.Package
}
Expand All @@ -65,6 +66,8 @@ func (cg *CRDGenerator) Generate(cfg *config.Resource) (string, error) {
if err != nil {
return "", errors.Wrapf(err, "cannot build types for %s", cfg.Kind)
}
cg.Generated = &gen

// TODO(muvaf): TypePrinter uses the given scope to see if the type exists
// before printing. We should ideally load the package in file system but
// loading the local package will result in error if there is
Expand Down
301 changes: 301 additions & 0 deletions pkg/pipeline/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/*
Copyright 2022 Upbound Inc.
*/

package pipeline

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"

"github.com/upbound/upjet/pkg/config"
"github.com/upbound/upjet/pkg/resource/json"
tjtypes "github.com/upbound/upjet/pkg/types"
)

var (
reRef = regexp.MustCompile(`\${(.+)}`)
reFile = regexp.MustCompile(`file\("(.+)"\)`)
)

type pavedWithManifest struct {
manifestPath string
paved *fieldpath.Paved
refsResolved bool
}

// ResourceExample represents the scraped example HCL configuration
// for a Terraform resource
type ResourceExample struct {
Manifest string `yaml:"manifest"`
References map[string]string `yaml:"references,omitempty"`
}

// Resource represents the scraped metadata for a Terraform resource
type Resource struct {
SubCategory string `yaml:"subCategory"`
Description string `yaml:"description,omitempty"`
Name string `yaml:"name"`
TitleName string `yaml:"titleName"`
Examples []ResourceExample `yaml:"examples,omitempty"`
ArgumentDocs map[string]string `yaml:"argumentDocs"`
ImportStatements []string `yaml:"importStatements"`
}

// ProviderMetadata metadata for a Terraform native provider
type ProviderMetadata struct {
Name string `yaml:"name"`
Resources map[string]*Resource `yaml:"resources"`
}

// NewProviderMetadataFromFile loads metadata from the specified YAML-formatted file
func NewProviderMetadataFromFile(path string) (*ProviderMetadata, error) {
buff, err := ioutil.ReadFile(filepath.Clean(path))
if err != nil {
return nil, errors.Wrapf(err, "failed to read metadata file %q", path)
}

metadata := &ProviderMetadata{}
return metadata, errors.Wrap(yaml.Unmarshal(buff, metadata), "failed to unmarshal provider metadata")
}

// ExampleGenerator represents a pipeline for generating example manifests.
// Generates example manifests for Terraform resources under examples-generated.
type ExampleGenerator struct {
rootDir string
resourceMeta map[string]*Resource
resources map[string]*pavedWithManifest
}

// NewExampleGenerator returns a configured ExampleGenerator
func NewExampleGenerator(rootDir string, resourceMeta map[string]*Resource) *ExampleGenerator {
return &ExampleGenerator{
rootDir: rootDir,
resourceMeta: resourceMeta,
resources: make(map[string]*pavedWithManifest),
}
}

// StoreExamples stores the generated example manifests under examples-generated in
// their respective API groups.
func (eg *ExampleGenerator) StoreExamples() error {
for n, pm := range eg.resources {
if err := eg.resolveReferencesOfPaved(pm); err != nil {
return errors.Wrapf(err, "cannot resolve references for resource: %s", n)
}
u := pm.paved.UnstructuredContent()
delete(u["spec"].(map[string]interface{})["forProvider"].(map[string]interface{}), "depends_on")
buff, err := yaml.Marshal(u)
if err != nil {
return errors.Wrapf(err, "cannot marshal example manifest for resource: %s", n)
}
manifestDir := filepath.Dir(pm.manifestPath)
if err := os.MkdirAll(manifestDir, 0750); err != nil {
return errors.Wrapf(err, "cannot mkdir %s", manifestDir)
}
// no sensitive info in the example manifest
if err := ioutil.WriteFile(pm.manifestPath, buff, 0644); err != nil { // nolint:gosec
return errors.Wrapf(err, "cannot write example manifest file %s for resource %s", pm.manifestPath, n)
}
}
return nil
}

func (eg *ExampleGenerator) resolveReferencesOfPaved(pm *pavedWithManifest) error {
if pm.refsResolved {
return nil
}
pm.refsResolved = true
return errors.Wrap(eg.resolveReferences(pm.paved.UnstructuredContent()), "failed to resolve references of paved")
}

func (eg *ExampleGenerator) resolveReferences(params map[string]interface{}) error { // nolint:gocyclo
for k, v := range params {
switch t := v.(type) {
case map[string]interface{}:
if err := eg.resolveReferences(t); err != nil {
return err
}

case []interface{}:
for _, e := range t {
eM, ok := e.(map[string]interface{})
if !ok {
continue
}
if err := eg.resolveReferences(eM); err != nil {
return err
}
}

case string:
g := reRef.FindStringSubmatch(t)
if len(g) != 2 {
continue
}
path := strings.Split(g[1], ".")
// expected reference format is <resource type>.<resource name>.<field name>
if len(path) < 3 {
continue
}
pm := eg.resources[path[0]]
if pm == nil || pm.paved == nil {
continue
}
if err := eg.resolveReferencesOfPaved(pm); err != nil {
return errors.Wrapf(err, "cannot recursively resolve references for %q", path[0])
}
pathStr := strings.Join(append([]string{"spec", "forProvider"}, path[2:]...), ".")
s, err := pm.paved.GetString(pathStr)
if fieldpath.IsNotFound(err) {
continue
}
if err != nil {
return errors.Wrapf(err, "cannot get string value from paved: %s", pathStr)
}
params[k] = s
}
}
return nil
}

// Generate generates an example manifest for the specified Terraform resource.
func (eg *ExampleGenerator) Generate(group, version string, r *config.Resource, fieldTransformations map[string]tjtypes.Transformation) error {
rm := eg.resourceMeta[r.Name]
if rm == nil || len(rm.Examples) == 0 {
return nil
}
var exampleParams map[string]interface{}
if err := json.TFParser.Unmarshal([]byte(rm.Examples[0].Manifest), &exampleParams); err != nil {
return errors.Wrapf(err, "cannot unmarshal example manifest for resource: %s", r.Name)
}
transformFields(exampleParams, r.ExternalName.OmittedFields, fieldTransformations, "")

example := map[string]interface{}{
"apiVersion": fmt.Sprintf("%s/%s", group, version),
"kind": r.Kind,
"metadata": map[string]interface{}{
"name": "example",
},
"spec": map[string]interface{}{
"forProvider": exampleParams,
"providerConfigRef": map[string]interface{}{
"name": "example",
},
},
}
manifestDir := filepath.Join(eg.rootDir, "examples-generated", strings.ToLower(strings.Split(group, ".")[0]))
eg.resources[r.Name] = &pavedWithManifest{
manifestPath: filepath.Join(manifestDir, fmt.Sprintf("%s.yaml", strings.ToLower(r.Kind))),
paved: fieldpath.Pave(example),
}
return nil
}

func getHierarchicalName(prefix, name string) string {
if prefix == "" {
return name
}
return fmt.Sprintf("%s.%s", prefix, name)
}

func transformFields(params map[string]interface{}, omittedFields []string, t map[string]tjtypes.Transformation, namePrefix string) { // nolint:gocyclo
for _, hn := range omittedFields {
for n := range params {
if hn == getHierarchicalName(namePrefix, n) {
delete(params, n)
break
}
}
}

for n, v := range params {
switch pT := v.(type) {
case map[string]interface{}:
transformFields(pT, omittedFields, t, getHierarchicalName(namePrefix, n))

case []interface{}:
for _, e := range pT {
eM, ok := e.(map[string]interface{})
if !ok {
continue
}
transformFields(eM, omittedFields, t, getHierarchicalName(namePrefix, n))
}
}
}

for hn, transform := range t {
for n, v := range params {
if hn == getHierarchicalName(namePrefix, n) {
delete(params, n)
if transform.IsRef {
if !transform.IsSensitive {
params[transform.TransformedName] = getRefField(v,
map[string]interface{}{
"name": "example",
})
} else {
secretName, secretKey := getSecretRef(v)
params[transform.TransformedName] = getRefField(v,
map[string]interface{}{
"name": secretName,
"namespace": "crossplane-system",
"key": secretKey,
})
}
} else {
params[transform.TransformedName] = v
}
break
}
}
}
}

func getRefField(v interface{}, ref map[string]interface{}) interface{} {
switch v.(type) {
case []interface{}:
return []interface{}{
ref,
}

default:
return ref
}
}

func getSecretRef(v interface{}) (string, string) {
secretName := "example-secret"
secretKey := "example-key"
s, ok := v.(string)
if !ok {
return secretName, secretKey
}
g := reRef.FindStringSubmatch(s)
if len(g) != 2 {
return secretName, secretKey
}
f := reFile.FindStringSubmatch(g[1])
switch {
case len(f) == 2: // then a file reference
_, file := filepath.Split(f[1])
secretKey = fmt.Sprintf("attribute.%s", file)
default:
parts := strings.Split(g[1], ".")
if len(parts) < 3 {
return secretName, secretKey
}
secretName = fmt.Sprintf("example-%s", strings.Join(strings.Split(parts[0], "_")[1:], "-"))
secretKey = fmt.Sprintf("attribute.%s", strings.Join(parts[2:], "."))
}
return secretName, secretKey
}
Loading

0 comments on commit 1a4a079

Please sign in to comment.