Skip to content

Commit

Permalink
Merge pull request #4 from pulumi/albert
Browse files Browse the repository at this point in the history
Add more language flags, fix minor bugs
  • Loading branch information
albert-zhong authored Sep 17, 2020
2 parents 6b01304 + 09ae3b5 commit 1a4084a
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 321 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PROJECT := github.com/pulumi/crd2pulumi
VERSION := 1.0.4
VERSION := 1.0.5
LDFLAGS := "-X 'github.com/pulumi/crd2pulumi/gen.Version=$(VERSION)'"

GO ?= go
Expand Down
90 changes: 69 additions & 21 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ const (
PythonPath string = "pythonPath"
)

var defaultOutputPath = "crds/"
const (
DotNetName string = "dotnetName"
GoName string = "goName"
NodeJSName string = "nodejsName"
PythonName string = "pythonName"
)

const defaultOutputPath = "crds/"

const long = `crd2pulumi is a CLI tool that generates typed Kubernetes
CustomResources to use in Pulumi programs, based on a
Expand All @@ -53,7 +60,9 @@ Notice that by just setting a language-specific output path (--pythonPath, --nod
still get generated, so setting -p, -n, etc becomes unnecessary.
`

func getLanguageSettings(flags *pflag.FlagSet) gen.LanguageSettings {
// NewLanguageSettings returns the parsed language settings given a set of flags. Also returns a list of notices for
// possibly misinterpreted flags.
func NewLanguageSettings(flags *pflag.FlagSet) (gen.LanguageSettings, []string) {
nodejs, _ := flags.GetBool(NodeJS)
python, _ := flags.GetBool(Python)
dotnet, _ := flags.GetBool(DotNet)
Expand All @@ -64,32 +73,55 @@ func getLanguageSettings(flags *pflag.FlagSet) gen.LanguageSettings {
dotnetPath, _ := flags.GetString(DotNetPath)
goPath, _ := flags.GetString(GoPath)

ls := gen.LanguageSettings{}
nodejsName, _ := flags.GetString(NodeJSName)
pythonName, _ := flags.GetString(PythonName)
dotNetName, _ := flags.GetString(DotNetName)
goName, _ := flags.GetString(GoName)

var notices []string
ls := gen.LanguageSettings{
NodeJSName: nodejsName,
PythonName: pythonName,
DotNetName: dotNetName,
GoName: goName,
}
if nodejsPath != "" {
ls.NodeJSPath = &nodejsPath
} else if nodejs {
if nodejs {
notices = append(notices, "-n is not necessary if --nodejsPath is already set")
}
} else if nodejs || nodejsName != gen.DefaultName {
path := filepath.Join(defaultOutputPath, NodeJS)
ls.NodeJSPath = &path
}
if pythonPath != "" {
ls.PythonPath = &pythonPath
} else if python {
if python {
notices = append(notices, "-p is not necessary if --pythonPath is already set")
}
} else if python || pythonName != gen.DefaultName {
path := filepath.Join(defaultOutputPath, Python)
ls.PythonPath = &path
}
if dotnetPath != "" {
ls.DotNetPath = &dotnetPath
} else if dotnet {
if dotnet {
notices = append(notices, "-d is not necessary if --dotnetPath is already set")
}
} else if dotnet || dotNetName != gen.DefaultName {
path := filepath.Join(defaultOutputPath, DotNet)
ls.DotNetPath = &path
}
if goPath != "" {
ls.GoPath = &goPath
} else if golang {
if golang {
notices = append(notices, "-g is not necessary if --goPath is already set")
}
} else if golang || goName != gen.DefaultName{
path := filepath.Join(defaultOutputPath, Go)
ls.GoPath = &path
}
return ls
return ls, notices
}

var (
Expand All @@ -100,8 +132,7 @@ var (
Example: example,
Version: gen.Version,
Args: func(cmd *cobra.Command, args []string) error {
emptyLanguageSettings := gen.LanguageSettings{}
if getLanguageSettings(cmd.Flags()) == emptyLanguageSettings {
if ls, _ := NewLanguageSettings(cmd.Flags()); !ls.GeneratesAtLeastOneLanguage() {
return errors.New("must specify at least one language")
}

Expand All @@ -114,9 +145,12 @@ var (
},
Run: func(cmd *cobra.Command, args []string) {
force, _ := cmd.Flags().GetBool("force")
languageSettings := getLanguageSettings(cmd.Flags())
ls, notices := NewLanguageSettings(cmd.Flags())
for _, notice := range notices {
fmt.Println("notice: " + notice)
}

err := gen.Generate(languageSettings, args, force)
err := gen.Generate(ls, args, force)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(-1)
Expand All @@ -134,17 +168,31 @@ func Execute() error {
var forceValue bool
var nodeJSValue, pythonValue, dotNetValue, goValue bool
var nodeJSPathValue, pythonPathValue, dotNetPathValue, goPathValue string
var nodeJSNameValue, pythonNameValue, dotNetNameValue, goNameValue string

func init() {
rootCmd.PersistentFlags().BoolVarP(&nodeJSValue, NodeJS, "n", false, "generate NodeJS")
rootCmd.PersistentFlags().BoolVarP(&pythonValue, Python, "p", false, "generate Python")
rootCmd.PersistentFlags().BoolVarP(&dotNetValue, DotNet, "d", false, "generate .NET")
rootCmd.PersistentFlags().BoolVarP(&goValue, Go, "g", false, "generate Go")
addBoolFlag := func(p *bool, name, shorthand string, value bool, usage string) {
rootCmd.PersistentFlags().BoolVarP(p, name, shorthand, value, usage)
}

addBoolFlag(&forceValue, "force", "f", false, "overwrite existing files")

addBoolFlag(&nodeJSValue, NodeJS, "n", false, "generate NodeJS")
addBoolFlag(&pythonValue, Python, "p", false, "generate Python")
addBoolFlag(&dotNetValue, DotNet, "d", false, "generate .NET")
addBoolFlag(&goValue, Go, "g", false, "generate Go")

addStringFlag := func(p *string, name string, value string, usage string) {
rootCmd.PersistentFlags().StringVar(p, name, value, usage)
}

rootCmd.PersistentFlags().StringVar(&nodeJSPathValue, NodeJSPath, "", "optional NodeJS output dir")
rootCmd.PersistentFlags().StringVar(&pythonPathValue, PythonPath, "", "optional Python output dir")
rootCmd.PersistentFlags().StringVar(&dotNetPathValue, DotNetPath, "", "optional .NET output dir")
rootCmd.PersistentFlags().StringVar(&goPathValue, GoPath, "", "optional Go output dir")
addStringFlag(&nodeJSPathValue, NodeJSPath, "", "optional NodeJS output dir")
addStringFlag(&pythonPathValue, PythonPath, "", "optional Python output dir")
addStringFlag(&dotNetPathValue, DotNetPath, "", "optional .NET output dir")
addStringFlag(&goPathValue, GoPath, "", "optional Go output dir")

rootCmd.PersistentFlags().BoolVarP(&forceValue, "force", "f", false, "overwrite existing files")
addStringFlag(&nodeJSNameValue, NodeJSName, gen.DefaultName, "name of NodeJS package")
addStringFlag(&pythonNameValue, PythonName, gen.DefaultName, "name of Python package")
addStringFlag(&dotNetNameValue, DotNetName, gen.DefaultName, "name of .NET package")
addStringFlag(&goNameValue, GoName, gen.DefaultName, "name of Go package")
}
28 changes: 17 additions & 11 deletions gen/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ var unneededDotNetFiles = []string{
"Provider.cs",
}

func (pg *PackageGenerator) genDotNet(outputDir string) error {
if files, err := pg.genDotNetFiles(); err != nil {
func (pg *PackageGenerator) genDotNet(outputDir, name string) error {
if files, err := pg.genDotNetFiles(name); err != nil {
return err
} else if err := writeFiles(files, outputDir); err != nil {
return err
}
return nil
}

func (pg *PackageGenerator) genDotNetFiles() (map[string]*bytes.Buffer, error) {
func (pg *PackageGenerator) genDotNetFiles(name string) (map[string]*bytes.Buffer, error) {
pkg := pg.SchemaPackageWithObjectMetaType()

// Set up C# namespaces
Expand All @@ -56,6 +56,8 @@ func (pg *PackageGenerator) genDotNetFiles() (map[string]*bytes.Buffer, error) {
// class defined in the .NET SDK is located at
// `Pulumi.Kubernetes.Types.Outputs.Meta.V1.ObjectMeta`. This path would
// only get generated properly if `compatibility` was `kubernetes20`.
oldName := pkg.Name
pkg.Name = name
pkg.Language["csharp"] = rawMessage(map[string]interface{}{
"packageReferences": map[string]string{
"Pulumi.Kubernetes": "2.*",
Expand All @@ -70,10 +72,11 @@ func (pg *PackageGenerator) genDotNetFiles() (map[string]*bytes.Buffer, error) {
return nil, errors.Wrap(err, "could not generate .NET package")
}

delete(pkg.Language, "chsarp")
pkg.Name = oldName
delete(pkg.Language, "csharp")

files["KubernetesResource.cs"] = []byte(kubernetesResource)
files["Utilities.cs"] = []byte(dotNetUtilities)
files["KubernetesResource.cs"] = []byte(kubernetesResource(name))
files["Utilities.cs"] = []byte(dotNetUtilities(name))

// Delete unneeded files
for _, unneededFile := range unneededDotNetFiles {
Expand All @@ -88,9 +91,9 @@ func (pg *PackageGenerator) genDotNetFiles() (map[string]*bytes.Buffer, error) {
return buffers, nil
}

var kubernetesResource = `// Copyright 2016-2020, Pulumi Corporation
namespace Pulumi.` + strings.Title(packageName) + `{
func kubernetesResource(name string) string {
return `// Copyright 2016-2020, Pulumi Corporation
namespace Pulumi.` + name + `{
/// <summary>
/// A base class for all Kubernetes resources.
/// </summary>
Expand All @@ -114,19 +117,21 @@ namespace Pulumi.` + strings.Title(packageName) + `{
}
}
`
}

// For some reason, we get a `Missing embedded version.txt file` error if we
// tried running `pulumi up` with the normal `Utilities.cs` file.
// As a temporary fix, this modified `Utilities.cs` file just removes the
// `static Utilities()` method.
var dotNetUtilities = `// *** WARNING: this file was generated by crd2pulumi. ***
func dotNetUtilities(name string) string {
return `// *** WARNING: this file was generated by crd2pulumi. ***
// *** Do not edit by hand unless you're certain you know what you are doing! ***
using System;
using System.Reflection;
using Pulumi.Kubernetes;
namespace Pulumi.` + strings.Title(packageName) + `
namespace Pulumi.` + name + `
{
static class Utilities
{
Expand Down Expand Up @@ -159,3 +164,4 @@ namespace Pulumi.` + strings.Title(packageName) + `
}
}
`
}
88 changes: 41 additions & 47 deletions gen/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,7 @@ const (
)

// Version specifies the crd2pulumi version. It should be set by the linker via LDFLAGS.
var Version string

// LanguageSettings contains the output paths for each language. If a path is
// null, then that language will not be generated at all.
type LanguageSettings struct {
NodeJSPath *string
PythonPath *string
DotNetPath *string
GoPath *string
}

// Returns true if at least one of the language-specific output paths already exists. If true, then a slice of the
// paths that already exist are also returned.
func (ls LanguageSettings) hasExistingPaths() (bool, []string) {
pathExists := func(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
var existingPaths []string
if ls.NodeJSPath != nil && pathExists(*ls.NodeJSPath) {
existingPaths = append(existingPaths, *ls.NodeJSPath)
}
if ls.PythonPath != nil && pathExists(*ls.PythonPath) {
existingPaths = append(existingPaths, *ls.PythonPath)
}
if ls.DotNetPath != nil && pathExists(*ls.DotNetPath) {
existingPaths = append(existingPaths, *ls.DotNetPath)
}
if ls.GoPath != nil && pathExists(*ls.GoPath) {
existingPaths = append(existingPaths, *ls.GoPath)
}
return len(existingPaths) > 0, existingPaths
}
var Version string = "1.0.0"

// Generate parses the CRDs at the given yamlPaths and outputs the generated
// code according to the language settings. Only overwrites existing files if
Expand All @@ -92,22 +60,22 @@ func Generate(ls LanguageSettings, yamlPaths []string, force bool) error {
}

if ls.NodeJSPath != nil {
if err := pg.genNodeJS(*ls.NodeJSPath); err != nil {
if err := pg.genNodeJS(*ls.NodeJSPath, ls.NodeJSName); err != nil {
return err
}
}
if ls.PythonPath != nil {
if err := pg.genPython(*ls.PythonPath); err != nil {
if err := pg.genPython(*ls.PythonPath, ls.PythonName); err != nil {
return err
}
}
if ls.GoPath != nil {
if err := pg.genGo(*ls.GoPath); err != nil {
if err := pg.genGo(*ls.GoPath, ls.GoName); err != nil {
return err
}
}
if ls.DotNetPath != nil {
if err := pg.genDotNet(*ls.DotNetPath); err != nil {
if err := pg.genDotNet(*ls.DotNetPath, ls.DotNetName); err != nil {
return err
}
}
Expand Down Expand Up @@ -171,6 +139,10 @@ func NewPackageGenerator(yamlPaths []string) (PackageGenerator, error) {
return PackageGenerator{}, errors.Wrapf(err, "could not unmarshal yaml file(s)")
}

if len(crds) == 0 {
return PackageGenerator{}, errors.New("could not find any CRD YAML files")
}

resourceTokensSize := 0
groupVersionsSize := 0

Expand Down Expand Up @@ -234,6 +206,16 @@ func (pg *PackageGenerator) moduleToPackage() map[string]string {
return moduleToPackage
}

// HasSchemas returns true if there exists at least one CustomResource with a schema in this package.
func (pg *PackageGenerator) HasSchemas() bool {
for _, crg := range pg.CustomResourceGenerators {
if crg.HasSchemas() {
return true
}
}
return false
}

// CustomResourceGenerator generates a Pulumi schema for a single CustomResource
type CustomResourceGenerator struct {
// CustomResourceDefinition contains the unmarshalled CRD YAML
Expand Down Expand Up @@ -261,21 +243,28 @@ type CustomResourceGenerator struct {

func NewCustomResourceGenerator(crd unstruct.Unstructured) (CustomResourceGenerator, error) {
apiVersion := crd.GetAPIVersion()

schemas := map[string]map[string]interface{}{}

validation, foundValidation, _ := unstruct.NestedMap(crd.Object, "spec", "validation", "openAPIV3Schema")
if foundValidation { // If present, use the top-level schema to validate all versions
versionMaps, _, _ := NestedMapSlice(crd.Object, "spec", "versions")
for _, version := range versionMaps {
name, _, _ := unstruct.NestedString(version, "name")
schemas[name] = validation
versionName, foundVersionName, _ := unstruct.NestedString(crd.Object, "spec", "version")
if foundVersionName {
schemas[versionName] = validation
} else if versionInfos, foundVersionInfos, _ := NestedMapSlice(crd.Object, "spec", "versions"); foundVersionInfos {
for _, versionInfo := range versionInfos {
versionName, _, _ := unstruct.NestedString(versionInfo, "name")
schemas[versionName] = validation
}
}
} else { // Otherwise use per-version schemas to validate each version
versionMaps, _, _ := NestedMapSlice(crd.Object, "spec", "versions")
for _, version := range versionMaps {
name, _, _ := unstruct.NestedString(version, "name")
schema, _, _ := unstruct.NestedMap(version, "schema", "openAPIV3Schema")
schemas[name] = schema
versionInfos, foundVersionInfos, _ := NestedMapSlice(crd.Object, "spec", "versions")
if foundVersionInfos {
for _, version := range versionInfos {
name, _, _ := unstruct.NestedString(version, "name")
if schema, foundSchema, _ := unstruct.NestedMap(version, "schema", "openAPIV3Schema"); foundSchema {
schemas[name] = schema
}
}
}
}

Expand Down Expand Up @@ -316,6 +305,11 @@ func NewCustomResourceGenerator(crd unstruct.Unstructured) (CustomResourceGenera
return crg, nil
}

// HasSchemas returns true if the CustomResource specifies at least some schema, and false otherwise.
func (crg *CustomResourceGenerator) HasSchemas() bool {
return len(crg.Schemas) > 0
}

// Returns the type token for a Kubernetes CustomResource with the given group,
// version, and kind.
func getToken(group, version, kind string) string {
Expand Down
Loading

0 comments on commit 1a4084a

Please sign in to comment.