From ba3131c8776180e3ee99fd3147fc1343bd6a22bf Mon Sep 17 00:00:00 2001 From: Sebastian Czech Date: Thu, 4 Apr 2024 14:59:32 +0200 Subject: [PATCH] feat: Render `service.go` for entry and config (#42) --- pkg/generate/generator.go | 2 + pkg/translate/funcs.go | 11 + pkg/translate/funcs_test.go | 24 ++ pkg/translate/imports.go | 31 ++ pkg/translate/imports_test.go | 25 ++ templates/sdk/entry.tmpl | 10 +- templates/sdk/location.tmpl | 8 +- templates/sdk/service.tmpl | 559 +++++++++++++++++++++++++++++++++- 8 files changed, 653 insertions(+), 17 deletions(-) create mode 100644 pkg/translate/imports.go create mode 100644 pkg/translate/imports_test.go diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index 7b6d925a..c623bb00 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -119,6 +119,7 @@ func (c *Creator) createFile(filePath string) (*os.File, error) { func (c *Creator) parseTemplate(templateName string) (*template.Template, error) { templatePath := filepath.Join(c.TemplatesDir, templateName) funcMap := template.FuncMap{ + "renderImports": translate.RenderImports, "packageName": translate.PackageName, "locationType": translate.LocationType, "specParamType": translate.SpecParamType, @@ -138,6 +139,7 @@ func (c *Creator) parseTemplate(templateName string) (*template.Template, error) "nestedSpecs": translate.NestedSpecs, "createGoSuffixFromVersion": translate.CreateGoSuffixFromVersion, "paramSupportedInVersion": translate.ParamSupportedInVersion, + "xmlPathSuffixes": translate.XmlPathSuffixes, } return template.New(templateName).Funcs(funcMap).ParseFiles(templatePath) } diff --git a/pkg/translate/funcs.go b/pkg/translate/funcs.go index 902434bc..1d8fb9ab 100644 --- a/pkg/translate/funcs.go +++ b/pkg/translate/funcs.go @@ -223,3 +223,14 @@ func argumentTypeForSpecMatchesFunction(parent []string, param *properties.SpecP strings.Join(parent, ""), param.Name.CamelCase) } } + +// XmlPathSuffixes return XML path suffixes created from profiles. +func XmlPathSuffixes(param *properties.SpecParam) []string { + xmlPathSuffixes := []string{} + if param.Profiles != nil { + for _, profile := range param.Profiles { + xmlPathSuffixes = append(xmlPathSuffixes, strings.Join(profile.Xpath, "/")) + } + } + return xmlPathSuffixes +} diff --git a/pkg/translate/funcs_test.go b/pkg/translate/funcs_test.go index a9eb00b6..328bb066 100644 --- a/pkg/translate/funcs_test.go +++ b/pkg/translate/funcs_test.go @@ -244,3 +244,27 @@ return true // then assert.Equal(t, expectedNestedSpec, renderedNestedSpecMatches) } + +func TestXmlPathSuffixes(t *testing.T) { + // given + spec := properties.Spec{ + Params: map[string]*properties.SpecParam{ + "a": { + Profiles: []*properties.SpecParamProfile{{ + Xpath: []string{"test", "a"}, + }}, + Name: &properties.NameVariant{ + Underscore: "a", + CamelCase: "A", + }, + }, + }, + } + expectedXpathSuffixes := []string{"test/a"} + + // when + actualXpathSuffixes := XmlPathSuffixes(spec.Params["a"]) + + // then + assert.Equal(t, expectedXpathSuffixes, actualXpathSuffixes) +} diff --git a/pkg/translate/imports.go b/pkg/translate/imports.go new file mode 100644 index 00000000..f4d0fec9 --- /dev/null +++ b/pkg/translate/imports.go @@ -0,0 +1,31 @@ +package translate + +import "github.com/paloaltonetworks/pan-os-codegen/pkg/imports" + +func RenderImports(templateType string) (string, error) { + manager := imports.NewManager() + + switch templateType { + case "entry": + manager.AddStandardImport("encoding/xml", "") + manager.AddStandardImport("fmt", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/filtering", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/generic", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/util", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/version", "") + case "location": + manager.AddStandardImport("fmt", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/errors", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/util", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/version", "") + case "service": + manager.AddStandardImport("context", "") + manager.AddStandardImport("fmt", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/errors", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/filtering", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/util", "") + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/xmlapi", "") + } + + return manager.RenderImports() +} diff --git a/pkg/translate/imports_test.go b/pkg/translate/imports_test.go new file mode 100644 index 00000000..366ca8c7 --- /dev/null +++ b/pkg/translate/imports_test.go @@ -0,0 +1,25 @@ +package translate + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRenderImports(t *testing.T) { + // given + expectedImports := ` +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +)` + + // when + actualImports, _ := RenderImports("location") + + // then + assert.NotNil(t, actualImports) + assert.Equal(t, expectedImports, actualImports) +} diff --git a/templates/sdk/entry.tmpl b/templates/sdk/entry.tmpl index acea588e..500ece69 100644 --- a/templates/sdk/entry.tmpl +++ b/templates/sdk/entry.tmpl @@ -1,15 +1,7 @@ {{- if .Entry}} package {{packageName .GoSdkPath}} - import ( - "encoding/xml" - "fmt" - - "github.com/PaloAltoNetworks/pango/filtering" - "github.com/PaloAltoNetworks/pango/generic" - "github.com/PaloAltoNetworks/pango/util" - "github.com/PaloAltoNetworks/pango/version" - ) + {{renderImports "entry"}} var ( _ filtering.Fielder = &Entry{} diff --git a/templates/sdk/location.tmpl b/templates/sdk/location.tmpl index e23bebde..3323db24 100644 --- a/templates/sdk/location.tmpl +++ b/templates/sdk/location.tmpl @@ -1,12 +1,6 @@ package {{packageName .GoSdkPath}} -import ( -"fmt" - -"github.com/PaloAltoNetworks/pango/errors" -"github.com/PaloAltoNetworks/pango/util" -"github.com/PaloAltoNetworks/pango/version" -) +{{renderImports "location"}} type Location struct { {{range $key, $location := .Locations}} diff --git a/templates/sdk/service.tmpl b/templates/sdk/service.tmpl index 3b3d1518..617a69f9 100644 --- a/templates/sdk/service.tmpl +++ b/templates/sdk/service.tmpl @@ -1 +1,558 @@ -package {{packageName .GoSdkPath}} \ No newline at end of file +package {{packageName .GoSdkPath}} +{{- if .Entry}} + {{renderImports "service"}} + + type Service struct { + client util.PangoClient + } + + func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } + } + + // Create adds new entry, then returns the result. + func (s *Service) Create(ctx context.Context, loc Location, entry Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, entry.Name) + if err != nil { + return nil, err + } + + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + + return s.Read(ctx, loc, entry.Name, "get") + } + + // Read returns the given config object, using the specified action ("get" or "show"). + func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + if name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, name) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return &list[0], nil + } + + // ReadFromConfig returns the given config object from the loaded XML config. + // Requires that client.LoadPanosConfig() has been invoked. + func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + if name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, name) + if err != nil { + return nil, err + } + + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to find 1 entry, got %d", len(list)) + } + + return &list[0], nil + } + + // Update updates the given config object, then returns the result. + func (s *Service) Update(ctx context.Context, loc Location, entry Entry, oldName string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + var old *Entry + if oldName != "" && oldName != entry.Name { + path, err := loc.Xpath(vn, oldName) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, oldName, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } + + if !SpecMatches(&entry, old) { + path, err := loc.Xpath(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + + return s.Read(ctx, loc, entry.Name, "get") + } + + // Delete deletes the given item. + func (s *Service) Delete(ctx context.Context, loc Location, name string) error { + if name == "" { + return errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + path, err := loc.Xpath(vn, name) + if err != nil { + return err + } + + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + return err + } + + // List returns a list of objects using the given action ("get" or "show"). + // Params filter and quote are for client side filtering. + func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "") + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(&x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil + } + + // ListFromConfig returns a list of objects at the given location. + // Requires that client.LoadPanosConfig() has been invoked. + // Params filter and quote are for client side filtering. + func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "") + if err != nil { + return nil, err + } + path = path[:len(path)-1] + + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(&x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil + } + + // ConfigureGroup performs all necessary set / edit / delete commands to ensure that the objects are configured as specified. + func (s *Service) ConfigureGroup(ctx context.Context, loc Location, entries []Entry, prevNames []string) ([]Entry, error) { + var err error + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(len(prevNames) + len(entries)) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + curObjs, err := s.List(ctx, loc, "get", "", "") + if err != nil { + return nil, err + } + + for _, entry := range entries { + var found bool + for _, live := range curObjs { + if entry.Name == live.Name { + found = true + if !SpecMatches(&entry, &live) { + path, err := loc.Xpath(vn, entry.Name) + if err != nil { + return nil, err + } + + elm, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: elm, + Target: s.client.GetTarget(), + }) + } + break + } + } + + if !found { + path, err := loc.Xpath(vn, entry.Name) + if err != nil { + return nil, err + } + + elm, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path), + Element: elm, + Target: s.client.GetTarget(), + }) + } + } + + if len(prevNames) != 0 { + for _, name := range prevNames { + var found bool + for _, entry := range entries { + if entry.Name == name { + found = true + break + } + } + + if !found { + path, err := loc.Xpath(vn, name) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + } + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + + curObjs, err = s.List(ctx, loc, "get", "", "") + if err != nil { + return nil, err + } + + ans := make([]Entry, 0, len(entries)) + for _, entry := range entries { + for _, live := range curObjs { + if entry.Name == live.Name { + ans = append(ans, live) + break + } + } + } + + return ans, nil + } +{{- else}} + import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" + ) + + type Service struct { + client util.PangoClient + } + + func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } + } + + // Create creates the given config object. + func (s *Service) Create(ctx context.Context, loc Location, config Config) (*Config, error) { + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + createSpec, err := specifier(config) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + + return s.Read(ctx, loc, "get") + } + + // Read returns the given config object, using the specified action ("get" or "show"). + func (s *Service) Read(ctx context.Context, loc Location, action string) (*Config, error) { + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return &list[0], nil + } + + // Delete deletes the given item. + func (s *Service) Delete(ctx context.Context, loc Location, config Config) error { + vn := s.client.Versioning() + + path, err := loc.Xpath(vn) + if err != nil { + return err + } + + deleteSuffixes := []string{ + {{- range $version := .SupportedVersions }} + {{- range $_, $param := $.Spec.Params}} + {{- if paramSupportedInVersion $param $version}} + {{- range $_, $profile := $param.Profiles}} + {{- range $_, $xpath := xmlPathSuffixes $param}} + "{{$xpath}}", + {{- end}} + {{- end}} + {{- end}} + {{- end}} + {{- range $_, $param := $.Spec.OneOf}} + {{- end}} + {{- end}} + } + + for _, suffix := range deleteSuffixes { + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(append(path, suffix)), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + if err != nil { + return err + } + } + return nil + } +{{- end}} \ No newline at end of file