From c2a4c8c1d880082bce110baafb0179616002a49f Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Sat, 4 May 2024 10:26:22 -0400 Subject: [PATCH] Add `plural cd services template` This will make it a much smoother experience to test .liquid or .tpl files as they are being developed. Should be functionally equivalent to the process done in-agent. --- .github/workflows/add-asana-comment.yaml | 19 ------ cmd/plural/cd_services.go | 63 +++++++++++++++++++ go.mod | 2 +- go.sum | 2 + pkg/cd/template/liquid.go | 64 ++++++++++++++++++++ pkg/cd/template/render.go | 30 +++++++++ pkg/cd/template/tpl.go | 19 ++++++ pkg/cd/template/utils.go | 77 ++++++++++++++++++++++++ 8 files changed, 256 insertions(+), 20 deletions(-) delete mode 100644 .github/workflows/add-asana-comment.yaml create mode 100644 pkg/cd/template/liquid.go create mode 100644 pkg/cd/template/render.go create mode 100644 pkg/cd/template/tpl.go create mode 100644 pkg/cd/template/utils.go diff --git a/.github/workflows/add-asana-comment.yaml b/.github/workflows/add-asana-comment.yaml deleted file mode 100644 index d144bb58a..000000000 --- a/.github/workflows/add-asana-comment.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Asana -on: - pull_request: - branches: [main] - types: [opened, closed, reopened] - -jobs: - create-comment-in-asana-task-job: - runs-on: ubuntu-latest - name: Create a comment in Asana Task - steps: - - name: Create a comment - uses: Asana/comment-on-task-github-action@latest - id: createComment - with: - asana-secret: ${{ secrets.ASANA_SECRET }} - comment-text: "{{PR_NAME}} is {{PR_STATE}}: {{PR_URL}}" - - name: Get status - run: echo "Status is ${{ steps.createComment.outputs.status }}" diff --git a/cmd/plural/cd_services.go b/cmd/plural/cd_services.go index 6c91cc433..a03b5d608 100644 --- a/cmd/plural/cd_services.go +++ b/cmd/plural/cd_services.go @@ -5,6 +5,7 @@ import ( "strings" gqlclient "github.com/pluralsh/console-client-go" + "github.com/pluralsh/plural-cli/pkg/cd/template" "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/polly/containers" @@ -95,6 +96,24 @@ func (p *Plural) cdServiceCommands() []cli.Command { }, Usage: "describe cluster service", }, + { + Name: "template", + Action: p.handleTemplateService, + Usage: "Dry-runs templating a .liquid or .tpl file with either a full service as params or custom config", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "service", + Usage: "specify the service you want to use as context while templating"}, + cli.StringFlag{ + Name: "configuration", + Usage: "hand-coded configuration for templating (useful if you want to test before creating a service)", + }, + cli.StringFlag{ + Name: "file", + Usage: "The .liquid or .tpl file you want to attempt to template.", + }, + }, + }, { Name: "delete", ArgsUsage: "SERVICE_ID", @@ -213,6 +232,50 @@ func (p *Plural) handleCreateClusterService(c *cli.Context) error { }) } +func (p *Plural) handleTemplateService(c *cli.Context) error { + if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { + return err + } + + printResult := func(out []byte) error { + fmt.Println() + fmt.Println(string(out)) + return nil + } + + if identifier := c.String("service"); identifier != "" { + serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(identifier) + if err != nil { + return err + } + + existing, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName) + if err != nil { + return err + } + if existing == nil { + return fmt.Errorf("Service %s does not exist", identifier) + } + + res, err := template.RenderService(c.String("file"), existing) + if err != nil { + return err + } + return printResult(res) + } + + bindings := map[string]interface{}{} + if err := utils.YamlFile(c.String("configuration"), &bindings); err != nil { + return err + } + + res, err := template.RenderYaml(c.String("file"), bindings) + if err != nil { + return err + } + return printResult(res) +} + func (p *Plural) handleCloneClusterService(c *cli.Context) error { if err := p.InitConsoleClient(consoleToken, consoleURL); err != nil { return err diff --git a/go.mod b/go.mod index 72188e3b4..5c6838155 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/packethost/packngo v0.29.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pluralsh/cluster-api-migration v0.2.15 - github.com/pluralsh/console-client-go v0.5.3 + github.com/pluralsh/console-client-go v0.5.5 github.com/pluralsh/gqlclient v1.11.0 github.com/pluralsh/plural-operator v0.5.5 github.com/pluralsh/polly v0.1.7 diff --git a/go.sum b/go.sum index 2f64a3802..e97e7cce2 100644 --- a/go.sum +++ b/go.sum @@ -1428,6 +1428,8 @@ github.com/pluralsh/console-client-go v0.1.17 h1:QMtnWdRvV13/sND/CFjFBUR8nyg3JJg github.com/pluralsh/console-client-go v0.1.17/go.mod h1:eyCiLA44YbXiYyJh8303jk5JdPkt9McgCo5kBjk4lKo= github.com/pluralsh/console-client-go v0.5.3 h1:RB4XtKlvh8+BM5o1o0h+W6zHculBGbL6q5lI/yRnqJE= github.com/pluralsh/console-client-go v0.5.3/go.mod h1:eyCiLA44YbXiYyJh8303jk5JdPkt9McgCo5kBjk4lKo= +github.com/pluralsh/console-client-go v0.5.5 h1:SvpI1bCQTUMA1VAdSHH5ESxr6r9xLxkbySlzxOTBO40= +github.com/pluralsh/console-client-go v0.5.5/go.mod h1:eyCiLA44YbXiYyJh8303jk5JdPkt9McgCo5kBjk4lKo= github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxDz4Q2VMpzprJIIKShxqG0E= github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= github.com/pluralsh/gqlclient v1.11.0 h1:FfXW7FiEJLHOfTAa7NxDb8jb3aMZNIpCAcG+bg8uHYA= diff --git a/pkg/cd/template/liquid.go b/pkg/cd/template/liquid.go new file mode 100644 index 000000000..aa9e303cc --- /dev/null +++ b/pkg/cd/template/liquid.go @@ -0,0 +1,64 @@ +package template + +import ( + "strings" + + "github.com/Masterminds/sprig/v3" + "github.com/osteele/liquid" + console "github.com/pluralsh/console-client-go" +) + +var ( + extensions = []string{".json", ".yaml", ".yml", ".yaml.liquid", ".yml.liquid", ".json.liquid"} + liquidEngine = liquid.NewEngine() + sprigFunctions = map[string]string{ + "toJson": "to_json", + "fromJson": "from_json", + "b64enc": "b64enc", + "b64dec": "b64dec", + "semverCompare": "semver_compare", + "sha256sum": "sha26sum", + "quote": "quote", + "squote": "squote", + "replace": "replace", + "coalesce": "coalesce", + } +) + +func init() { + fncs := sprig.TxtFuncMap() + for key, name := range sprigFunctions { + liquidEngine.RegisterFilter(name, fncs[key]) + } + liquidEngine.RegisterFilter("indent", indent) + liquidEngine.RegisterFilter("nindent", nindent) + liquidEngine.RegisterFilter("replace", strings.ReplaceAll) + + liquidEngine.RegisterFilter("default", dfault) + liquidEngine.RegisterFilter("ternary", ternary) +} + +func renderLiquid(input []byte, bindings map[string]interface{}) ([]byte, error) { + return liquidEngine.ParseAndRender(input, bindings) +} + +func clusterConfiguration(cluster *console.BaseClusterFragment) map[string]interface{} { + res := map[string]interface{}{ + "ID": cluster.ID, + "Self": cluster.Self, + "Handle": cluster.Handle, + "Name": cluster.Name, + "Version": cluster.Version, + "CurrentVersion": cluster.CurrentVersion, + "KasUrl": cluster.KasURL, + "Metadata": cluster.Metadata, + } + + for k, v := range res { + res[strings.ToLower(k)] = v + } + res["kasUrl"] = cluster.KasURL + res["currentVersion"] = cluster.CurrentVersion + + return res +} diff --git a/pkg/cd/template/render.go b/pkg/cd/template/render.go new file mode 100644 index 000000000..18155eb88 --- /dev/null +++ b/pkg/cd/template/render.go @@ -0,0 +1,30 @@ +package template + +import ( + "os" + "strings" + + console "github.com/pluralsh/console-client-go" +) + +func RenderYaml(path string, bindings map[string]interface{}) ([]byte, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + if strings.HasPrefix(path, ".tpl") { + return renderTpl(content, bindings) + } + + return renderLiquid(content, bindings) +} + +func RenderService(path string, svc *console.ServiceDeploymentExtended) ([]byte, error) { + bindings := map[string]interface{}{ + "Configuration": configMap(svc), + "Cluster": clusterConfiguration(svc.Cluster), + "Contexts": contexts(svc), + } + return RenderYaml(path, bindings) +} diff --git a/pkg/cd/template/tpl.go b/pkg/cd/template/tpl.go new file mode 100644 index 000000000..2ff3791dd --- /dev/null +++ b/pkg/cd/template/tpl.go @@ -0,0 +1,19 @@ +package template + +import ( + "bytes" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +func renderTpl(input []byte, bindings map[string]interface{}) ([]byte, error) { + tpl, err := template.New("gotpl").Funcs(sprig.TxtFuncMap()).Parse(string(input)) + if err != nil { + return nil, err + } + + var buffer bytes.Buffer + err = tpl.Execute(&buffer, bindings) + return buffer.Bytes(), err +} diff --git a/pkg/cd/template/utils.go b/pkg/cd/template/utils.go new file mode 100644 index 000000000..a1b67ae09 --- /dev/null +++ b/pkg/cd/template/utils.go @@ -0,0 +1,77 @@ +package template + +import ( + "reflect" + "strings" + + console "github.com/pluralsh/console-client-go" +) + +func configMap(svc *console.ServiceDeploymentExtended) map[string]string { + res := map[string]string{} + for _, config := range svc.Configuration { + res[config.Name] = config.Value + } + + return res +} + +func contexts(svc *console.ServiceDeploymentExtended) map[string]map[string]interface{} { + res := map[string]map[string]interface{}{} + for _, context := range svc.Contexts { + res[context.Name] = context.Configuration + } + return res +} + +func indent(v string, spaces int) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.ReplaceAll(v, "\n", "\n"+pad) +} + +func nindent(v string, spaces int) string { + return "\n" + indent(v, spaces) +} + +func ternary(v bool, vt interface{}, vf interface{}) interface{} { + if v { + return vt + } + + return vf +} + +func dfault(v1, v2 interface{}) interface{} { + if empty(v1) { + return v2 + } + + return v1 +} + +func empty(given interface{}) bool { + g := reflect.ValueOf(given) + if !g.IsValid() { + return true + } + + // Basically adapted from text/template.isTrue + switch g.Kind() { + default: + return g.IsNil() + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return g.Len() == 0 + case reflect.Bool: + return !g.Bool() + case reflect.Complex64, reflect.Complex128: + return g.Complex() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return g.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return g.Uint() == 0 + case reflect.Float32, reflect.Float64: + return g.Float() == 0 + case reflect.Struct: + return false + } +}