2ed92e0c6c
This pull request was automerged via backport-assistant
1107 lines
22 KiB
Go
1107 lines
22 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package jobspec2
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/helper/pointer"
|
|
"github.com/hashicorp/nomad/jobspec"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEquivalentToHCL1(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hclSpecDir := "../jobspec/test-fixtures/"
|
|
fis, err := os.ReadDir(hclSpecDir)
|
|
require.NoError(t, err)
|
|
|
|
for _, fi := range fis {
|
|
name := fi.Name()
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
f, err := os.Open(hclSpecDir + name)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
|
|
job1, err := jobspec.Parse(f)
|
|
if err != nil {
|
|
t.Skip("file is not parsable in v1")
|
|
}
|
|
|
|
f.Seek(0, 0)
|
|
|
|
job2, err := Parse(name, f)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, job1, job2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEquivalentToHCL1_ComplexConfig(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
name := "./test-fixtures/config-compatibility.hcl"
|
|
f, err := os.Open(name)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
|
|
job1, err := jobspec.Parse(f)
|
|
require.NoError(t, err)
|
|
|
|
f.Seek(0, 0)
|
|
|
|
job2, err := Parse(name, f)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, job1, job2)
|
|
}
|
|
|
|
func TestParse_ConnectJob(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
name := "./test-fixtures/connect-example.hcl"
|
|
f, err := os.Open(name)
|
|
must.NoError(t, err)
|
|
t.Cleanup(func() { _ = f.Close() })
|
|
|
|
job2, err := Parse(name, f)
|
|
must.NoError(t, err)
|
|
|
|
timeout := job2.TaskGroups[0].Services[0].Connect.SidecarService.Proxy.Upstreams[0].Config["connect_timeout_ms"]
|
|
must.Eq(t, 9999, timeout)
|
|
}
|
|
|
|
func TestParse_VarsAndFunctions(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variables {
|
|
region_var = "default"
|
|
}
|
|
job "example" {
|
|
datacenters = [for s in ["dc1", "dc2"] : upper(s)]
|
|
region = var.region_var
|
|
}
|
|
`
|
|
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{"region_var=aug"},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"DC1", "DC2"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "aug", *out.Region)
|
|
}
|
|
|
|
func TestParse_VariablesDefaultsAndSet(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variables {
|
|
region_var = "default_region"
|
|
}
|
|
|
|
variable "dc_var" {
|
|
default = "default_dc"
|
|
}
|
|
|
|
job "example" {
|
|
datacenters = [var.dc_var]
|
|
region = var.region_var
|
|
}
|
|
`
|
|
|
|
t.Run("defaults", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"default_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "default_region", *out.Region)
|
|
})
|
|
|
|
t.Run("set via -var args", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{"dc_var=set_dc", "region_var=set_region"},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"set_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "set_region", *out.Region)
|
|
})
|
|
|
|
t.Run("set via envvars", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
Envs: []string{
|
|
"NOMAD_VAR_dc_var=set_dc",
|
|
"NOMAD_VAR_region_var=set_region",
|
|
},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"set_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "set_region", *out.Region)
|
|
})
|
|
|
|
t.Run("set via var-files", func(t *testing.T) {
|
|
varFile, err := os.CreateTemp("", "")
|
|
require.NoError(t, err)
|
|
defer os.Remove(varFile.Name())
|
|
|
|
content := `dc_var = "set_dc"
|
|
region_var = "set_region"`
|
|
_, err = varFile.WriteString(content)
|
|
require.NoError(t, err)
|
|
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
VarFiles: []string{varFile.Name()},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"set_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "set_region", *out.Region)
|
|
})
|
|
|
|
t.Run("var-file does not exist", func(t *testing.T) {
|
|
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
VarFiles: []string{"does-not-exist.hcl"},
|
|
AllowFS: true,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, out)
|
|
})
|
|
}
|
|
|
|
// TestParse_UnknownVariables asserts that unknown variables are left intact for further processing
|
|
func TestParse_UnknownVariables(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variables {
|
|
region_var = "default"
|
|
}
|
|
job "example" {
|
|
datacenters = [for s in ["dc1", "dc2"] : upper(s)]
|
|
region = var.region_var
|
|
meta {
|
|
known_var = "${var.region_var}"
|
|
unknown_var = "${UNKNOWN}"
|
|
}
|
|
}
|
|
`
|
|
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{"region_var=aug"},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
meta := map[string]string{
|
|
"known_var": "aug",
|
|
"unknown_var": "${UNKNOWN}",
|
|
}
|
|
|
|
require.Equal(t, meta, out.Meta)
|
|
}
|
|
|
|
// TestParse_UnsetVariables asserts that variables that have neither types nor
|
|
// values return early instead of panicking.
|
|
func TestParse_UnsetVariables(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variable "region_var" {}
|
|
job "example" {
|
|
datacenters = [for s in ["dc1", "dc2"] : upper(s)]
|
|
region = var.region_var
|
|
}
|
|
`
|
|
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{},
|
|
AllowFS: true,
|
|
})
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Unset variable")
|
|
}
|
|
|
|
func TestParse_Locals(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variables {
|
|
region_var = "default_region"
|
|
}
|
|
|
|
locals {
|
|
# literal local
|
|
dc = "local_dc"
|
|
# local that depends on a variable
|
|
region = "${var.region_var}.example"
|
|
}
|
|
|
|
job "example" {
|
|
datacenters = [local.dc]
|
|
region = local.region
|
|
}
|
|
`
|
|
|
|
t.Run("defaults", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"local_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "default_region.example", *out.Region)
|
|
})
|
|
|
|
t.Run("set via -var argments", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{"region_var=set_region"},
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, []string{"local_dc"}, out.Datacenters)
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, "set_region.example", *out.Region)
|
|
})
|
|
}
|
|
|
|
func TestParse_FileOperators(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
job "example" {
|
|
region = file("parse_test.go")
|
|
}
|
|
`
|
|
|
|
t.Run("enabled", func(t *testing.T) {
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: nil,
|
|
AllowFS: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
expected, err := os.ReadFile("parse_test.go")
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, out.Region)
|
|
require.Equal(t, string(expected), *out.Region)
|
|
})
|
|
|
|
t.Run("disabled", func(t *testing.T) {
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: nil,
|
|
AllowFS: false,
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "filesystem function disabled")
|
|
})
|
|
}
|
|
|
|
func TestParseDynamic(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
job "example" {
|
|
|
|
dynamic "group" {
|
|
for_each = [
|
|
{ name = "groupA", idx = 1 },
|
|
{ name = "groupB", idx = 2 },
|
|
{ name = "groupC", idx = 3 },
|
|
]
|
|
labels = [group.value.name]
|
|
|
|
content {
|
|
count = group.value.idx
|
|
|
|
service {
|
|
port = group.value.name
|
|
}
|
|
|
|
task "simple" {
|
|
driver = "raw_exec"
|
|
config {
|
|
command = group.value.name
|
|
}
|
|
meta {
|
|
VERSION = group.value.idx
|
|
}
|
|
env {
|
|
ID = format("id:%s", group.value.idx)
|
|
}
|
|
resources {
|
|
cpu = group.value.idx
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: nil,
|
|
AllowFS: false,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, out.TaskGroups, 3)
|
|
require.Equal(t, "groupA", *out.TaskGroups[0].Name)
|
|
require.Equal(t, "groupB", *out.TaskGroups[1].Name)
|
|
require.Equal(t, "groupC", *out.TaskGroups[2].Name)
|
|
require.Equal(t, 1, *out.TaskGroups[0].Tasks[0].Resources.CPU)
|
|
require.Equal(t, "groupA", out.TaskGroups[0].Services[0].PortLabel)
|
|
|
|
// interpolation inside maps
|
|
require.Equal(t, "groupA", out.TaskGroups[0].Tasks[0].Config["command"])
|
|
require.Equal(t, "1", out.TaskGroups[0].Tasks[0].Meta["VERSION"])
|
|
require.Equal(t, "id:1", out.TaskGroups[0].Tasks[0].Env["ID"])
|
|
require.Equal(t, "id:2", out.TaskGroups[1].Tasks[0].Env["ID"])
|
|
require.Equal(t, "3", out.TaskGroups[2].Tasks[0].Meta["VERSION"])
|
|
}
|
|
|
|
func TestParse_InvalidHCL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
t.Run("invalid body", func(t *testing.T) {
|
|
hcl := `invalid{hcl`
|
|
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
ArgVars: []string{},
|
|
AllowFS: true,
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid vars file", func(t *testing.T) {
|
|
tmp, err := os.CreateTemp("", "nomad-jobspec2-")
|
|
require.NoError(t, err)
|
|
defer os.Remove(tmp.Name())
|
|
|
|
vars := `invalid{hcl`
|
|
_, err = tmp.Write([]byte(vars))
|
|
require.NoError(t, err)
|
|
|
|
hcl := `
|
|
variables {
|
|
region_var = "default"
|
|
}
|
|
job "example" {
|
|
datacenters = [for s in ["dc1", "dc2"] : upper(s)]
|
|
region = var.region_var
|
|
}
|
|
`
|
|
|
|
_, err = ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
VarFiles: []string{tmp.Name()},
|
|
ArgVars: []string{},
|
|
AllowFS: true,
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestParse_InvalidScalingSyntax(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
expectedErr string
|
|
hcl string
|
|
}{
|
|
{
|
|
"valid",
|
|
"",
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
max = 40
|
|
type = "horizontal"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "cpu" {
|
|
max = 20
|
|
}
|
|
scaling "mem" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
"group missing max",
|
|
`argument "max" is required`,
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
#max = 40
|
|
type = "horizontal"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "cpu" {
|
|
max = 20
|
|
}
|
|
scaling "mem" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
"group invalid type",
|
|
`task group scaling policy had invalid type`,
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
max = 40
|
|
type = "invalid_type"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "cpu" {
|
|
max = 20
|
|
}
|
|
scaling "mem" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
"task invalid label",
|
|
`scaling policy name must be "cpu" or "mem"`,
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
max = 40
|
|
type = "horizontal"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "not_cpu" {
|
|
max = 20
|
|
}
|
|
scaling "mem" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
"task duplicate blocks",
|
|
`Duplicate scaling "cpu" block`,
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
max = 40
|
|
type = "horizontal"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "cpu" {
|
|
max = 20
|
|
}
|
|
scaling "cpu" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
"task invalid type",
|
|
`Invalid scaling policy type`,
|
|
`
|
|
job "example" {
|
|
group "g1" {
|
|
scaling {
|
|
max = 40
|
|
type = "horizontal"
|
|
}
|
|
|
|
task "t1" {
|
|
scaling "cpu" {
|
|
max = 20
|
|
type = "invalid"
|
|
}
|
|
scaling "mem" {
|
|
max = 15
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: c.name + ".hcl",
|
|
Body: []byte(c.hcl),
|
|
AllowFS: false,
|
|
})
|
|
if c.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseJob_JobWithFunctionsAndLookups(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
variable "env" {
|
|
description = "target environment for the job"
|
|
}
|
|
|
|
locals {
|
|
environments = {
|
|
prod = { count = 20, dcs = ["prod-dc1", "prod-dc2"] },
|
|
staging = { count = 3, dcs = ["dc1"] },
|
|
}
|
|
|
|
env = lookup(local.environments, var.env, { count = 0, dcs = [] })
|
|
}
|
|
|
|
job "job-webserver" {
|
|
datacenters = local.env.dcs
|
|
group "group-webserver" {
|
|
count = local.env.count
|
|
|
|
task "server" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "hashicorp/http-echo"
|
|
args = ["-text", "Hello from ${var.env}"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
cases := []struct {
|
|
env string
|
|
expectedJob *api.Job
|
|
}{
|
|
{
|
|
"prod",
|
|
&api.Job{
|
|
ID: pointer.Of("job-webserver"),
|
|
Name: pointer.Of("job-webserver"),
|
|
Datacenters: []string{"prod-dc1", "prod-dc2"},
|
|
TaskGroups: []*api.TaskGroup{
|
|
{
|
|
Name: pointer.Of("group-webserver"),
|
|
Count: pointer.Of(20),
|
|
|
|
Tasks: []*api.Task{
|
|
{
|
|
Name: "server",
|
|
Driver: "docker",
|
|
|
|
Config: map[string]interface{}{
|
|
"image": "hashicorp/http-echo",
|
|
"args": []interface{}{"-text", "Hello from prod"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"staging",
|
|
&api.Job{
|
|
ID: pointer.Of("job-webserver"),
|
|
Name: pointer.Of("job-webserver"),
|
|
Datacenters: []string{"dc1"},
|
|
TaskGroups: []*api.TaskGroup{
|
|
{
|
|
Name: pointer.Of("group-webserver"),
|
|
Count: pointer.Of(3),
|
|
|
|
Tasks: []*api.Task{
|
|
{
|
|
Name: "server",
|
|
Driver: "docker",
|
|
|
|
Config: map[string]interface{}{
|
|
"image": "hashicorp/http-echo",
|
|
"args": []interface{}{"-text", "Hello from staging"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"unknown",
|
|
&api.Job{
|
|
ID: pointer.Of("job-webserver"),
|
|
Name: pointer.Of("job-webserver"),
|
|
Datacenters: []string{},
|
|
TaskGroups: []*api.TaskGroup{
|
|
{
|
|
Name: pointer.Of("group-webserver"),
|
|
Count: pointer.Of(0),
|
|
|
|
Tasks: []*api.Task{
|
|
{
|
|
Name: "server",
|
|
Driver: "docker",
|
|
|
|
Config: map[string]interface{}{
|
|
"image": "hashicorp/http-echo",
|
|
"args": []interface{}{"-text", "Hello from unknown"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.env, func(t *testing.T) {
|
|
found, err := ParseWithConfig(&ParseConfig{
|
|
Path: "example.hcl",
|
|
Body: []byte(hcl),
|
|
AllowFS: false,
|
|
ArgVars: []string{"env=" + c.env},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expectedJob, found)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_TaskEnvs(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
envSnippet string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
"none",
|
|
``,
|
|
nil,
|
|
},
|
|
{
|
|
"block",
|
|
`
|
|
env {
|
|
key = "value"
|
|
} `,
|
|
map[string]string{"key": "value"},
|
|
},
|
|
{
|
|
"attribute",
|
|
`
|
|
env = {
|
|
"key.dot" = "val1"
|
|
key_unquoted_without_dot = "val2"
|
|
} `,
|
|
map[string]string{"key.dot": "val1", "key_unquoted_without_dot": "val2"},
|
|
},
|
|
{
|
|
"attribute_colons",
|
|
`env = {
|
|
"key.dot" : "val1"
|
|
key_unquoted_without_dot : "val2"
|
|
} `,
|
|
map[string]string{"key.dot": "val1", "key_unquoted_without_dot": "val2"},
|
|
},
|
|
{
|
|
"attribute_empty",
|
|
`env = {}`,
|
|
map[string]string{},
|
|
},
|
|
{
|
|
"attribute_expression",
|
|
`env = {for k in ["a", "b"]: k => "val-${k}" }`,
|
|
map[string]string{"a": "val-a", "b": "val-b"},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
hcl := `
|
|
job "example" {
|
|
group "group" {
|
|
task "task" {
|
|
driver = "docker"
|
|
config {}
|
|
|
|
` + c.envSnippet + `
|
|
}
|
|
}
|
|
}`
|
|
|
|
out, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, c.expected, out.TaskGroups[0].Tasks[0].Env)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_TaskEnvs_Multiple(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := `
|
|
job "example" {
|
|
group "group" {
|
|
task "task" {
|
|
driver = "docker"
|
|
config {}
|
|
|
|
env = {"a": "b"}
|
|
env {
|
|
c = "d"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Duplicate env block")
|
|
}
|
|
|
|
func Test_TaskEnvs_Invalid(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
envSnippet string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
"attr: invalid expression",
|
|
`env = { key = local.undefined_local }`,
|
|
`does not have an attribute named "undefined_local"`,
|
|
},
|
|
{
|
|
"block: invalid block expression",
|
|
`env {
|
|
for k in ["a", "b"]: k => k
|
|
}`,
|
|
"Invalid block definition",
|
|
},
|
|
{
|
|
"attr: not make sense",
|
|
`env = [ "a" ]`,
|
|
"Unsuitable value: map of string required",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
hcl := `
|
|
job "example" {
|
|
group "group" {
|
|
task "task" {
|
|
driver = "docker"
|
|
config {}
|
|
|
|
` + c.envSnippet + `
|
|
}
|
|
}
|
|
}`
|
|
_, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_Meta_Alternatives(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := ` job "example" {
|
|
group "group" {
|
|
task "task" {
|
|
driver = "config"
|
|
config {}
|
|
|
|
meta {
|
|
source = "task"
|
|
}
|
|
}
|
|
|
|
meta {
|
|
source = "group"
|
|
|
|
}
|
|
}
|
|
|
|
meta {
|
|
source = "job"
|
|
}
|
|
}
|
|
`
|
|
|
|
asBlock, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
hclAsAttr := strings.ReplaceAll(hcl, "meta {", "meta = {")
|
|
require.Equal(t, 3, strings.Count(hclAsAttr, "meta = {"))
|
|
|
|
asAttr, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hclAsAttr),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, asBlock, asAttr)
|
|
require.Equal(t, map[string]string{"source": "job"}, asBlock.Meta)
|
|
require.Equal(t, map[string]string{"source": "group"}, asBlock.TaskGroups[0].Meta)
|
|
require.Equal(t, map[string]string{"source": "task"}, asBlock.TaskGroups[0].Tasks[0].Meta)
|
|
|
|
}
|
|
|
|
// TestParse_UndefinedVariables asserts that values with undefined variables are left
|
|
// intact in the job representation
|
|
func TestParse_UndefinedVariables(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
cases := []string{
|
|
"plain",
|
|
"foo-${BAR}",
|
|
"foo-${attr.network.dev-us-east1-relay-vpc.external-ip.0}",
|
|
`${env["BLAH"]}`,
|
|
`${mixed-indexing.0[3]["FOO"].5}`,
|
|
`with spaces ${ root. field[ "FOO"].5 }`,
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c, func(t *testing.T) {
|
|
hcl := `job "example" {
|
|
region = "` + c + `"
|
|
}`
|
|
|
|
job, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, c, *job.Region)
|
|
})
|
|
}
|
|
|
|
t.Run("unquoted", func(t *testing.T) {
|
|
hcl := `job "example" {
|
|
region = meta.mytest
|
|
}`
|
|
|
|
job, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "${meta.mytest}", *job.Region)
|
|
|
|
})
|
|
}
|
|
|
|
func TestParseServiceCheck(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hcl := ` job "group_service_check_script" {
|
|
group "group" {
|
|
service {
|
|
name = "foo-service"
|
|
port = "http"
|
|
check {
|
|
name = "check-name"
|
|
type = "http"
|
|
method = "POST"
|
|
body = "{\"check\":\"mem\"}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
parsedJob, err := ParseWithConfig(&ParseConfig{
|
|
Path: "input.hcl",
|
|
Body: []byte(hcl),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
expectedJob := &api.Job{
|
|
ID: pointer.Of("group_service_check_script"),
|
|
Name: pointer.Of("group_service_check_script"),
|
|
TaskGroups: []*api.TaskGroup{
|
|
{
|
|
Name: pointer.Of("group"),
|
|
Services: []*api.Service{
|
|
{
|
|
Name: "foo-service",
|
|
PortLabel: "http",
|
|
Checks: []api.ServiceCheck{
|
|
{
|
|
Name: "check-name",
|
|
Type: "http",
|
|
Method: "POST",
|
|
Body: "{\"check\":\"mem\"}",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
require.Equal(t, expectedJob, parsedJob)
|
|
}
|
|
|
|
func TestWaitConfig(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
hclBytes, err := os.ReadFile("test-fixtures/template-wait-config.hcl")
|
|
require.NoError(t, err)
|
|
|
|
job, err := ParseWithConfig(&ParseConfig{
|
|
Path: "test-fixtures/template-wait-config.hcl",
|
|
Body: hclBytes,
|
|
AllowFS: false,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
tmpl := job.TaskGroups[0].Tasks[0].Templates[0]
|
|
require.NotNil(t, tmpl)
|
|
require.NotNil(t, tmpl.Wait)
|
|
require.Equal(t, 5*time.Second, *tmpl.Wait.Min)
|
|
require.Equal(t, 60*time.Second, *tmpl.Wait.Max)
|
|
}
|
|
|
|
func TestErrMissingKey(t *testing.T) {
|
|
ci.Parallel(t)
|
|
hclBytes, err := os.ReadFile("test-fixtures/template-err-missing-key.hcl")
|
|
require.NoError(t, err)
|
|
job, err := ParseWithConfig(&ParseConfig{
|
|
Path: "test-fixtures/template-err-missing-key.hcl",
|
|
Body: hclBytes,
|
|
AllowFS: false,
|
|
})
|
|
require.NoError(t, err)
|
|
tmpl := job.TaskGroups[0].Tasks[0].Templates[0]
|
|
require.NotNil(t, tmpl)
|
|
require.NotNil(t, tmpl.ErrMissingKey)
|
|
require.True(t, *tmpl.ErrMissingKey)
|
|
}
|
|
|
|
func TestRestartRenderTemplates(t *testing.T) {
|
|
ci.Parallel(t)
|
|
hclBytes, err := os.ReadFile("test-fixtures/restart-render-templates.hcl")
|
|
require.NoError(t, err)
|
|
job, err := ParseWithConfig(&ParseConfig{
|
|
Path: "test-fixtures/restart-render-templates.hcl",
|
|
Body: hclBytes,
|
|
AllowFS: false,
|
|
})
|
|
require.NoError(t, err)
|
|
tg := job.TaskGroups[0]
|
|
require.NotNil(t, tg.RestartPolicy)
|
|
require.True(t, *tg.RestartPolicy.RenderTemplates)
|
|
|
|
require.Nil(t, tg.Tasks[0].RestartPolicy)
|
|
require.False(t, *tg.Tasks[1].RestartPolicy.RenderTemplates)
|
|
}
|