diff --git a/pkg/content/unmarshal.go b/pkg/content/unmarshal.go new file mode 100644 index 00000000..8135969a --- /dev/null +++ b/pkg/content/unmarshal.go @@ -0,0 +1,25 @@ +package content + +import ( + "encoding/json" + "fmt" + "gopkg.in/yaml.v3" + "strings" +) + +func Unmarshal(v []byte, o interface{}) error { + var err error + + runes := []byte(strings.TrimSpace(string(v))) + if len(runes) == 0 { + return fmt.Errorf("no data in file") + } + + if runes[0] == '{' && runes[len(runes)-1] == '}' { + err = json.Unmarshal(v, o) + } else { + err = yaml.Unmarshal(v, o) + } + + return err +} diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go new file mode 100644 index 00000000..fa67a141 --- /dev/null +++ b/pkg/generate/generator.go @@ -0,0 +1,123 @@ +package generate + +import ( + "fmt" + "github.com/paloaltonetworks/pan-os-codegen/pkg/properties" + "github.com/paloaltonetworks/pan-os-codegen/pkg/translate" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "text/template" +) + +type Creator struct { + GoOutputDir string + TemplatesDir string + Spec *properties.Normalization +} + +func NewCreator(goOutputDir string, templatesDir string, spec *properties.Normalization) *Creator { + return &Creator{ + GoOutputDir: goOutputDir, + TemplatesDir: templatesDir, + Spec: spec, + } +} + +func (c *Creator) RenderTemplate() error { + templates := make([]string, 0, 100) + templates, err := c.listOfTemplates(templates) + if err != nil { + return err + } + + for _, templateName := range templates { + filePath := c.createFullFilePath(c.GoOutputDir, c.Spec, templateName) + fmt.Printf("Create file %s\n", filePath) + + if err := c.makeAllDirs(filePath, err); err != nil { + return err + } + + outputFile, err := c.createFile(filePath) + if err != nil { + return err + } + defer func(outputFile *os.File) { + err := outputFile.Close() + if err != nil { + + } + }(outputFile) + + tmpl, err := c.parseTemplate(templateName) + if err != nil { + return err + } + + err = c.generateOutputFileFromTemplate(tmpl, outputFile, c.Spec) + if err != nil { + return err + } + } + return nil +} + +func (c *Creator) generateOutputFileFromTemplate(tmpl *template.Template, output io.Writer, spec *properties.Normalization) error { + if err := tmpl.Execute(output, spec); err != nil { + return err + } + return nil +} + +func (c *Creator) parseTemplate(templateName string) (*template.Template, error) { + templatePath := fmt.Sprintf("%s/%s", c.TemplatesDir, templateName) + funcMap := template.FuncMap{ + "packageName": translate.PackageName, + } + tmpl, err := template.New(templateName).Funcs(funcMap).ParseFiles(templatePath) + if err != nil { + return nil, err + } + return tmpl, nil +} + +func (c *Creator) createFullFilePath(goOutputDir string, spec *properties.Normalization, templateName string) string { + return fmt.Sprintf("%s/%s/%s.go", goOutputDir, strings.Join(spec.GoSdkPath, "/"), strings.Split(templateName, ".")[0]) +} + +func (c *Creator) listOfTemplates(files []string) ([]string, error) { + err := filepath.WalkDir(c.TemplatesDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if strings.HasSuffix(entry.Name(), ".tmpl") { + files = append(files, filepath.Base(path)) + } + + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +func (c *Creator) createFile(filePath string) (*os.File, error) { + outputFile, err := os.Create(filePath) + if err != nil { + return nil, err + } + return outputFile, nil +} + +func (c *Creator) makeAllDirs(filePath string, err error) error { + dirPath := filepath.Dir(filePath) + if err = os.MkdirAll(dirPath, os.ModePerm); err != nil { + return err + } + return nil +} diff --git a/pkg/generate/generator_test.go b/pkg/generate/generator_test.go new file mode 100644 index 00000000..816cc90e --- /dev/null +++ b/pkg/generate/generator_test.go @@ -0,0 +1,68 @@ +package generate + +import ( + "bytes" + "github.com/paloaltonetworks/pan-os-codegen/pkg/properties" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCreateFullFilePath(t *testing.T) { + // given + spec := properties.Normalization{ + GoSdkPath: []string{"go"}, + } + generator := NewCreator("test", "../../templates/sdk", &spec) + + // when + fullFilePath := generator.createFullFilePath("output", &spec, "template.tmpl") + + // then + assert.NotNil(t, fullFilePath) + assert.Equal(t, "output/go/template.go", fullFilePath) +} + +// NOTE - unit tests should only touch code inside package, do not reference external resources. +// Nevertheless, below tests ARE REFERENCING to text templates, because template ARE NOT EMBEDDED into Go files. +// Technically we could embed them, but then: +// 1 - we are losing clarity of the code, +// 2 - we are mixing Golang with templates expressions. +// Testing generator is crucial, so below tests we can br treated more as integration tests, not unit one. + +func TestListOfTemplates(t *testing.T) { + // given + spec := properties.Normalization{ + GoSdkPath: []string{"go"}, + } + generator := NewCreator("test", "../../templates/sdk", &spec) + + // when + var templates []string + templates, _ = generator.listOfTemplates(templates) + + // then + assert.Equal(t, 4, len(templates)) +} + +func TestParseTemplate(t *testing.T) { + // given + spec := properties.Normalization{ + GoSdkPath: []string{"object", "address"}, + } + generator := NewCreator("test", "../../templates/sdk", &spec) + expectedFileContent := `package address + +type Specifier func(Entry) (any, error) + +type Normalizer interface { + Normalize() ([]Entry, error) +}` + + // when + template, _ := generator.parseTemplate("interfaces.tmpl") + var output bytes.Buffer + _ = generator.generateOutputFileFromTemplate(template, &output, generator.Spec) + + // then + assert.Equal(t, expectedFileContent, output.String()) +} diff --git a/pkg/load/file.go b/pkg/load/file.go index 8904753d..7c89daf3 100644 --- a/pkg/load/file.go +++ b/pkg/load/file.go @@ -1,30 +1,9 @@ package load import ( - "encoding/json" - "fmt" - "gopkg.in/yaml.v3" "os" - "strings" ) -func Unmarshal(v []byte, o interface{}) error { - var err error - - runes := []byte(strings.TrimSpace(string(v))) - if len(runes) == 0 { - return fmt.Errorf("no data in file") - } - - if runes[0] == '{' && runes[len(runes)-1] == '}' { - err = json.Unmarshal(v, o) - } else { - err = yaml.Unmarshal(v, o) - } - - return err -} - func File(path string) ([]byte, error) { content, err := os.ReadFile(path) if err != nil { diff --git a/pkg/mktp/cmd.go b/pkg/mktp/cmd.go index dac57003..c87012bc 100644 --- a/pkg/mktp/cmd.go +++ b/pkg/mktp/cmd.go @@ -3,6 +3,7 @@ package mktp import ( "context" "fmt" + "github.com/paloaltonetworks/pan-os-codegen/pkg/generate" "github.com/paloaltonetworks/pan-os-codegen/pkg/load" "io" "os" @@ -61,37 +62,49 @@ func (c *Cmd) Execute() error { //providerDataSources := make([]string, 0, 200) //providerResources := make([]string, 0, 100) - fmt.Fprintf(c.Stdout, "Reading configuration file: %s...\n", c.args[0]) - content, err := load.File(c.args[0]) - config, err := properties.ParseConfig(content) - if err != nil { - return fmt.Errorf("error parsing %s - %s", c.args[0], err) + // Check if path to configuration file is passed as argument + if len(c.args) == 0 { + return fmt.Errorf("path to configuration file is required") } + configPath := c.args[0] - fmt.Fprintf(c.Stdout, "Output directory for Go SDK: %s\n", config.Output.GoSdk) - if err = os.MkdirAll(config.Output.GoSdk, 0755); err != nil && !os.IsExist(err) { - return err + // Load configuration file + content, err := load.File(configPath) + if err != nil { + return fmt.Errorf("error loading %s - %s", configPath, err) } - fmt.Fprintf(c.Stdout, "Output directory for Terraform provider: %s\n", config.Output.TerraformProvider) - if err = os.MkdirAll(config.Output.TerraformProvider, 0755); err != nil && !os.IsExist(err) { - return err + // Parse configuration file + config, err := properties.ParseConfig(content) + if err != nil { + return fmt.Errorf("error parsing %s - %s", configPath, err) } - for _, configPath := range c.specs { - fmt.Fprintf(c.Stdout, "Parsing %s...\n", configPath) - content, err := load.File(configPath) + for _, specPath := range c.specs { + fmt.Fprintf(c.Stdout, "Parsing %s...\n", specPath) + + // Load YAML file + content, err := load.File(specPath) + if err != nil { + return fmt.Errorf("error loading %s - %s", specPath, err) + } + + // Parse content spec, err := properties.ParseSpec(content) if err != nil { - return fmt.Errorf("error parsing %s - %s", configPath, err) + return fmt.Errorf("error parsing %s - %s", specPath, err) } // Sanity check. if err = spec.Sanity(); err != nil { - return fmt.Errorf("%s sanity failed: %s", configPath, err) + return fmt.Errorf("%s sanity failed: %s", specPath, err) } // Output normalization as pango code. + generator := generate.NewCreator(config.Output.GoSdk, "templates/sdk", spec) + if err = generator.RenderTemplate(); err != nil { + return fmt.Errorf("error rendering %s - %s", specPath, err) + } // Output as Terraform code. } diff --git a/pkg/properties/config.go b/pkg/properties/config.go index 23e8468e..7846d1cf 100644 --- a/pkg/properties/config.go +++ b/pkg/properties/config.go @@ -1,8 +1,6 @@ package properties -import ( - "github.com/paloaltonetworks/pan-os-codegen/pkg/load" -) +import "github.com/paloaltonetworks/pan-os-codegen/pkg/content" type Config struct { Output OutputPaths `json:"output" yaml:"output"` @@ -13,8 +11,8 @@ type OutputPaths struct { TerraformProvider string `json:"terraform_provider" yaml:"terraform_provider"` } -func ParseConfig(content []byte) (*Config, error) { +func ParseConfig(input []byte) (*Config, error) { var ans Config - err := load.Unmarshal(content, &ans) + err := content.Unmarshal(input, &ans) return &ans, err } diff --git a/pkg/properties/normalized.go b/pkg/properties/normalized.go index 5ec2b1f8..2d99d67e 100644 --- a/pkg/properties/normalized.go +++ b/pkg/properties/normalized.go @@ -3,12 +3,11 @@ package properties import ( "errors" "fmt" + "github.com/paloaltonetworks/pan-os-codegen/pkg/content" "io/fs" "path/filepath" "runtime" "strings" - - "github.com/paloaltonetworks/pan-os-codegen/pkg/load" ) type Normalization struct { @@ -128,9 +127,9 @@ func GetNormalizations() ([]string, error) { return files, nil } -func ParseSpec(content []byte) (*Normalization, error) { +func ParseSpec(input []byte) (*Normalization, error) { var ans Normalization - err := load.Unmarshal(content, &ans) + err := content.Unmarshal(input, &ans) return &ans, err } diff --git a/pkg/properties/normalized_test.go b/pkg/properties/normalized_test.go index ec07cb47..b6a01fb3 100644 --- a/pkg/properties/normalized_test.go +++ b/pkg/properties/normalized_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -const content = `name: 'Address' +const sampleSpec = `name: 'Address' terraform_provider_suffix: 'address' go_sdk_path: - 'objects' @@ -129,7 +129,7 @@ func TestUnmarshallAddressSpecFile(t *testing.T) { // given // when - yamlParsedData, _ := ParseSpec([]byte(content)) + yamlParsedData, _ := ParseSpec([]byte(sampleSpec)) // then assert.NotNilf(t, yamlParsedData, "Unmarshalled data cannot be nil") @@ -290,7 +290,7 @@ spec: ` // when - yamlParsedData, _ := ParseSpec([]byte(content)) + yamlParsedData, _ := ParseSpec([]byte(sampleSpec)) yamlDump, _ := yaml.Marshal(&yamlParsedData) //fmt.Printf("%s", string(yamlDump)) @@ -312,7 +312,7 @@ func TestGetNormalizations(t *testing.T) { func TestSanity(t *testing.T) { // given - var fileContent = ` + var sampleInvalidSpec = ` name: 'Address' terraform_provider_suffix: 'address' go_sdk_path: @@ -323,7 +323,7 @@ xpath_suffix: ` // when yamlParsedData := Normalization{} - err := yaml.Unmarshal([]byte(fileContent), &yamlParsedData) + err := yaml.Unmarshal([]byte(sampleInvalidSpec), &yamlParsedData) if err != nil { t.Fatalf("error: %v", err) } @@ -335,7 +335,7 @@ xpath_suffix: func TestValidation(t *testing.T) { // given - var fileContent = ` + var sampleInvalidSpec = ` name: 'Address' terraform_provider_suffix: 'address' xpath_suffix: @@ -343,7 +343,7 @@ xpath_suffix: ` // when yamlParsedData := Normalization{} - err := yaml.Unmarshal([]byte(fileContent), &yamlParsedData) + err := yaml.Unmarshal([]byte(sampleInvalidSpec), &yamlParsedData) if err != nil { t.Fatalf("error: %v", err) } diff --git a/pkg/translate/names.go b/pkg/translate/names.go new file mode 100644 index 00000000..d584fce1 --- /dev/null +++ b/pkg/translate/names.go @@ -0,0 +1,9 @@ +package translate + +// Get package name from Go SDK path +func PackageName(list []string) string { + if len(list) == 0 { + return "" + } + return list[len(list)-1] +} diff --git a/pkg/translate/names_test.go b/pkg/translate/names_test.go new file mode 100644 index 00000000..f0a7c2cc --- /dev/null +++ b/pkg/translate/names_test.go @@ -0,0 +1,17 @@ +package translate + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPackageName(t *testing.T) { + // given + sampleGoSdkPath := []string{"objects", "address"} + + // when + packageName := PackageName(sampleGoSdkPath) + + // then + assert.Equal(t, "address", packageName) +} diff --git a/templates/sdk/entry.tmpl b/templates/sdk/entry.tmpl new file mode 100644 index 00000000..7f410c4c --- /dev/null +++ b/templates/sdk/entry.tmpl @@ -0,0 +1 @@ +package {{ packageName .GoSdkPath }} \ No newline at end of file diff --git a/templates/sdk/interfaces.tmpl b/templates/sdk/interfaces.tmpl new file mode 100644 index 00000000..00a72dcc --- /dev/null +++ b/templates/sdk/interfaces.tmpl @@ -0,0 +1,7 @@ +package {{ packageName .GoSdkPath }} + +type Specifier func(Entry) (any, error) + +type Normalizer interface { + Normalize() ([]Entry, error) +} \ No newline at end of file diff --git a/templates/sdk/location.tmpl b/templates/sdk/location.tmpl new file mode 100644 index 00000000..0c27f6b2 --- /dev/null +++ b/templates/sdk/location.tmpl @@ -0,0 +1,9 @@ +package {{ packageName .GoSdkPath }} + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) diff --git a/templates/sdk/service.tmpl b/templates/sdk/service.tmpl new file mode 100644 index 00000000..7f410c4c --- /dev/null +++ b/templates/sdk/service.tmpl @@ -0,0 +1 @@ +package {{ packageName .GoSdkPath }} \ No newline at end of file