From a50f086078f2bafc4bb24777a0e623b44f43c3b4 Mon Sep 17 00:00:00 2001 From: Alex Dulin Date: Wed, 3 Mar 2021 10:23:51 -0500 Subject: [PATCH] feat: Add support for using jobspec2 parser This adds in support for using the HCL2 jobspec2 parser with a new -hcl2 flag. This allows for using new features like `upstreams.datacenter` and terminating gateways. It should also allow for new features in the future to be used that are only being added to the new jobspec2 parser. Signed-off-by: Alex Dulin --- command/deploy.go | 6 +- command/deploy_test.go | 4 +- command/dispatch.go | 4 +- command/plan.go | 8 ++- command/render.go | 4 +- go.sum | 4 ++ levant/structs/config.go | 3 + template/render.go | 12 +++- template/render_test.go | 32 ++++++--- .../single_templated_connect.nomad | 72 +++++++++++++++++++ test/acctest/deploy.go | 2 +- 11 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 template/test-fixtures/single_templated_connect.nomad diff --git a/command/deploy.go b/command/deploy.go index c3817d226..79f3b92a5 100644 --- a/command/deploy.go +++ b/command/deploy.go @@ -88,6 +88,9 @@ General Options: Used in conjunction with the -job-file will deploy a templated job to your Nomad cluster. You can repeat this flag multiple times to supply multiple var-files. [default: levant.(json|yaml|yml|tf)] + + -hcl2 + Use HCL2 jopspec parser. ` return strings.TrimSpace(helpText) } @@ -125,6 +128,7 @@ func (c *DeployCommand) Run(args []string) int { flags.StringVar(&format, "log-format", "HUMAN", "") flags.StringVar(&config.Deploy.VaultToken, "vault-token", "", "") flags.BoolVar(&config.Deploy.EnvVault, "vault", false, "") + flags.BoolVar(&config.Template.HCL2, "hcl2", false, "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") @@ -159,7 +163,7 @@ func (c *DeployCommand) Run(args []string) int { } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, - config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) + config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars, config.Template.HCL2) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) return 1 diff --git a/command/deploy_test.go b/command/deploy_test.go index 97e7825a3..e46526ab2 100644 --- a/command/deploy_test.go +++ b/command/deploy_test.go @@ -30,7 +30,7 @@ func TestDeploy_checkCanaryAutoPromote(t *testing.T) { } for i, c := range cases { - job, err := template.RenderJob(c.File, []string{}, "", &fVars) + job, err := template.RenderJob(c.File, []string{}, "", &fVars, false) if err != nil { t.Fatalf("case %d failed: %v", i, err) } @@ -61,7 +61,7 @@ func TestDeploy_checkForceBatch(t *testing.T) { } for i, c := range cases { - job, err := template.RenderJob(c.File, []string{}, "", &fVars) + job, err := template.RenderJob(c.File, []string{}, "", &fVars, false) if err != nil { t.Fatalf("case %d failed: %v", i, err) } diff --git a/command/dispatch.go b/command/dispatch.go index 3a3e05c27..fe442943b 100644 --- a/command/dispatch.go +++ b/command/dispatch.go @@ -25,7 +25,7 @@ Usage: levant dispatch [options] [input source] Dispatch creates an instance of a parameterized job. A data payload to the dispatched instance can be provided via stdin by using "-" or by specifying a path to a file. Metadata can be supplied by using the meta flag one or more - times. + times. General Options: @@ -46,7 +46,7 @@ Dispatch Options: -meta = Meta takes a key/value pair separated by "=". The metadata key will be merged into the job's metadata. The job may define a default value for the - key which is overridden when dispatching. The flag can be provided more + key which is overridden when dispatching. The flag can be provided more than once to inject multiple metadata key/value pairs. Arbitrary keys are not allowed. The parameterized job must allow the key to be merged. ` diff --git a/command/plan.go b/command/plan.go index caee6d907..2353fc6e2 100644 --- a/command/plan.go +++ b/command/plan.go @@ -41,7 +41,7 @@ General Options: -allow-stale Allow stale consistency mode for requests into nomad. - + -consul-address= The Consul host and port to use when making Consul KeyValue lookups for template rendering. @@ -68,6 +68,9 @@ General Options: Used in conjunction with the -job-file will plan a templated job against your Nomad cluster. You can repeat this flag multiple times to supply multiple var-files. [default: levant.(json|yaml|yml|tf)] + + -hcl2 + Use HCL2 jopspec parser. ` return strings.TrimSpace(helpText) } @@ -97,6 +100,7 @@ func (c *PlanCommand) Run(args []string) int { flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "") flags.StringVar(&level, "log-level", "INFO", "") flags.StringVar(&format, "log-format", "HUMAN", "") + flags.BoolVar(&config.Template.HCL2, "hcl2", false, "") flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "") if err = flags.Parse(args); err != nil { @@ -124,7 +128,7 @@ func (c *PlanCommand) Run(args []string) int { } config.Template.Job, err = template.RenderJob(config.Template.TemplateFile, - config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars) + config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars, config.Template.HCL2) if err != nil { c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err)) diff --git a/command/render.go b/command/render.go index 03066c0cc..4eb1118bc 100644 --- a/command/render.go +++ b/command/render.go @@ -23,8 +23,8 @@ func (c *RenderCommand) Help() string { Usage: levant render [options] [TEMPLATE] Render a Nomad job template, useful for debugging. Like deploy, the render - command also supports passing variables individually on the command line. - Multiple vars can be passed in the format of -var 'key=value'. Variables + command also supports passing variables individually on the command line. + Multiple vars can be passed in the format of -var 'key=value'. Variables passed via the command line take precedence over the same variable declared within a passed variable file. diff --git a/go.sum b/go.sum index 1924a4f4e..b30040ab8 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,7 @@ github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZy github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M= github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= @@ -123,6 +124,7 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= @@ -356,6 +358,7 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-connlimit v0.2.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VVPGNyVPnUX8AqS0= github.com/hashicorp/go-connlimit v0.3.0/go.mod h1:OUj9FGL1tPIhl/2RCfzYHrIiWj+VVPGNyVPnUX8AqS0= +github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840 h1:kgvybwEeu0SXktbB2y3uLHX9lklLo+nzUwh59A3jzQc= github.com/hashicorp/go-cty-funcs v0.0.0-20200930094925-2721b1e36840/go.mod h1:Abjk0jbRkDaNCzsRhOv2iDCofYpX1eVsjozoiK63qLA= github.com/hashicorp/go-discover v0.0.0-20191202160150-7ec2cfbda7a2/go.mod h1:NnH5X4UCBEBdTuK2L8s4e4ilJm3UmGX0bANHCz0HSs0= github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f/go.mod h1:D4eo8/CN92vm9/9UDG+ldX1/fMFa4kpl8qzyTolus8o= @@ -794,6 +797,7 @@ github.com/zclconf/go-cty v1.5.1/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0 github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0= github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/levant/structs/config.go b/levant/structs/config.go index dba4fde32..ce8c227c1 100644 --- a/levant/structs/config.go +++ b/levant/structs/config.go @@ -83,6 +83,9 @@ type TemplateConfig struct { // VariableFiles contains the variables which will be substituted into the // templateFile before deployment. VariableFiles []string + + // HCL2 is a boolean flag that enables using jobspec2 parser + HCL2 bool } // ScaleConfig contains all the scaling specific configuration options. diff --git a/template/render.go b/template/render.go index 9552d7e0e..8ab45da45 100644 --- a/template/render.go +++ b/template/render.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/levant/helper" nomad "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/jobspec" + "github.com/hashicorp/nomad/jobspec2" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/hcl2shim" "github.com/rs/zerolog/log" @@ -19,13 +20,22 @@ import ( // RenderJob takes in a template and variables performing a render of the // template followed by Nomad jobspec parse. -func RenderJob(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}) (job *nomad.Job, err error) { +func RenderJob(templateFile string, variableFiles []string, addr string, flagVars *map[string]interface{}, hcl2 bool) (job *nomad.Job, err error) { var tpl *bytes.Buffer tpl, err = RenderTemplate(templateFile, variableFiles, addr, flagVars) if err != nil { return } + if hcl2 { + return jobspec2.ParseWithConfig(&jobspec2.ParseConfig{ + Path: templateFile, + Body: tpl.Bytes(), + AllowFS: false, + Strict: true, + }) + } + return jobspec.Parse(tpl) } diff --git a/template/render_test.go b/template/render_test.go index 09bfd6853..5c07756d3 100644 --- a/template/render_test.go +++ b/template/render_test.go @@ -25,7 +25,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { fVars := make(map[string]interface{}) // Test basic TF template render. - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf"}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -37,7 +37,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test basic YAML template render. - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -49,7 +49,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test multiple var-files - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.yaml", "test-fixtures/test-overwrite.yaml"}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -58,7 +58,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test multiple var-files of different types - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -68,7 +68,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { // Test multiple var-files with var-args fVars["job_name"] = testJobNameOverwrite2 - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{"test-fixtures/test.tf", "test-fixtures/test-overwrite.yaml"}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -77,7 +77,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { } // Test empty var-args and empty variable file render. - job, err = RenderJob("test-fixtures/none_templated.nomad", []string{}, "", &fVars) + job, err = RenderJob("test-fixtures/none_templated.nomad", []string{}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -87,7 +87,7 @@ func TestTemplater_RenderTemplate(t *testing.T) { // Test var-args only render. fVars = map[string]interface{}{"job_name": testJobName, "task_resource_cpu": "1313"} - job, err = RenderJob("test-fixtures/single_templated.nomad", []string{}, "", &fVars) + job, err = RenderJob("test-fixtures/single_templated.nomad", []string{}, "", &fVars, false) if err != nil { t.Fatal(err) } @@ -98,11 +98,27 @@ func TestTemplater_RenderTemplate(t *testing.T) { t.Fatalf("expected CPU resource %v but got %v", 1313, *job.TaskGroups[0].Tasks[0].Resources.CPU) } + // Test var-args only render with HCL2 spec + fVars = map[string]interface{}{"job_name": testJobName, "task_resource_cpu": "1313", "upstream_datacenter": "dc2"} + job, err = RenderJob("test-fixtures/single_templated_connect.nomad", []string{}, "", &fVars, true) + if err != nil { + t.Fatal(err) + } + if *job.Name != testJobName { + t.Fatalf("expected %s but got %v", testJobName, *job.Name) + } + if *job.TaskGroups[0].Tasks[0].Resources.CPU != 1313 { + t.Fatalf("expected CPU resource %v but got %v", 1313, *job.TaskGroups[0].Tasks[0].Resources.CPU) + } + if job.TaskGroups[0].Services[0].Connect.SidecarService.Proxy.Upstreams[0].Datacenter != "dc2" { + t.Fatalf("expected connect upstream datacenter %v but got %v", "dc2", job.TaskGroups[0].Services[0].Connect.SidecarService.Proxy.Upstreams[0].Datacenter) + } + // Test var-args and variables file render. delete(fVars, "job_name") fVars["datacentre"] = testDCName os.Setenv(testEnvName, testEnvValue) - job, err = RenderJob("test-fixtures/multi_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars) + job, err = RenderJob("test-fixtures/multi_templated.nomad", []string{"test-fixtures/test.yaml"}, "", &fVars, false) if err != nil { t.Fatal(err) } diff --git a/template/test-fixtures/single_templated_connect.nomad b/template/test-fixtures/single_templated_connect.nomad new file mode 100644 index 000000000..0e09b89f9 --- /dev/null +++ b/template/test-fixtures/single_templated_connect.nomad @@ -0,0 +1,72 @@ +job "[[.job_name]]" { + datacenters = ["dc1"] + type = "service" + update { + max_parallel = 1 + min_healthy_time = "10s" + healthy_deadline = "1m" + auto_revert = true + } + + group "cache" { + count = 1 + restart { + attempts = 10 + interval = "5m" + delay = "25s" + mode = "delay" + } + ephemeral_disk { + size = 300 + } + network { + mode = "bridge" + } + service { + name = "global-redis-check" + tags = ["global", "cache"] + port = "6379" + + connect { + sidecar_service { + proxy { + upstreams { + destination_name = "foobar" + local_bind_port = 9200 + datacenter = "[[ .upstream_datacenter ]]" + } + } + } + } + + check { + name = "alive" + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + task "redis" { + template { + data = <