github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/jobspec/parse_test.go (about)

     1  package jobspec
     2  
     3  import (
     4  	"path/filepath"
     5  	"reflect"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/nomad/nomad/structs"
    11  
    12  	"github.com/hashicorp/consul/api"
    13  )
    14  
    15  func TestParse(t *testing.T) {
    16  	cases := []struct {
    17  		File   string
    18  		Result *structs.Job
    19  		Err    bool
    20  	}{
    21  		{
    22  			"basic.hcl",
    23  			&structs.Job{
    24  				ID:          "binstore-storagelocker",
    25  				Name:        "binstore-storagelocker",
    26  				Type:        "service",
    27  				Priority:    50,
    28  				AllAtOnce:   true,
    29  				Datacenters: []string{"us2", "eu1"},
    30  				Region:      "global",
    31  				VaultToken:  "foo",
    32  
    33  				Meta: map[string]string{
    34  					"foo": "bar",
    35  				},
    36  
    37  				Constraints: []*structs.Constraint{
    38  					&structs.Constraint{
    39  						LTarget: "kernel.os",
    40  						RTarget: "windows",
    41  						Operand: "=",
    42  					},
    43  				},
    44  
    45  				Update: structs.UpdateStrategy{
    46  					Stagger:     60 * time.Second,
    47  					MaxParallel: 2,
    48  				},
    49  
    50  				TaskGroups: []*structs.TaskGroup{
    51  					&structs.TaskGroup{
    52  						Name:          "outside",
    53  						Count:         1,
    54  						EphemeralDisk: structs.DefaultEphemeralDisk(),
    55  						Tasks: []*structs.Task{
    56  							&structs.Task{
    57  								Name:   "outside",
    58  								Driver: "java",
    59  								Config: map[string]interface{}{
    60  									"jar_path": "s3://my-cool-store/foo.jar",
    61  								},
    62  								Meta: map[string]string{
    63  									"my-cool-key": "foobar",
    64  								},
    65  								LogConfig: structs.DefaultLogConfig(),
    66  							},
    67  						},
    68  					},
    69  
    70  					&structs.TaskGroup{
    71  						Name:  "binsl",
    72  						Count: 5,
    73  						Constraints: []*structs.Constraint{
    74  							&structs.Constraint{
    75  								LTarget: "kernel.os",
    76  								RTarget: "linux",
    77  								Operand: "=",
    78  							},
    79  						},
    80  						Meta: map[string]string{
    81  							"elb_mode":     "tcp",
    82  							"elb_interval": "10",
    83  							"elb_checks":   "3",
    84  						},
    85  						RestartPolicy: &structs.RestartPolicy{
    86  							Interval: 10 * time.Minute,
    87  							Attempts: 5,
    88  							Delay:    15 * time.Second,
    89  							Mode:     "delay",
    90  						},
    91  						EphemeralDisk: &structs.EphemeralDisk{
    92  							Sticky: true,
    93  							SizeMB: 150,
    94  						},
    95  						Tasks: []*structs.Task{
    96  							&structs.Task{
    97  								Name:   "binstore",
    98  								Driver: "docker",
    99  								User:   "bob",
   100  								Config: map[string]interface{}{
   101  									"image": "hashicorp/binstore",
   102  									"labels": []map[string]interface{}{
   103  										map[string]interface{}{
   104  											"FOO": "bar",
   105  										},
   106  									},
   107  								},
   108  								Services: []*structs.Service{
   109  									{
   110  										Name:      "binstore-storagelocker-binsl-binstore",
   111  										Tags:      []string{"foo", "bar"},
   112  										PortLabel: "http",
   113  										Checks: []*structs.ServiceCheck{
   114  											{
   115  												Name:      "check-name",
   116  												Type:      "tcp",
   117  												PortLabel: "admin",
   118  												Interval:  10 * time.Second,
   119  												Timeout:   2 * time.Second,
   120  											},
   121  										},
   122  									},
   123  								},
   124  								Env: map[string]string{
   125  									"HELLO": "world",
   126  									"LOREM": "ipsum",
   127  								},
   128  								Resources: &structs.Resources{
   129  									CPU:      500,
   130  									MemoryMB: 128,
   131  									IOPS:     0,
   132  									Networks: []*structs.NetworkResource{
   133  										&structs.NetworkResource{
   134  											MBits:         100,
   135  											ReservedPorts: []structs.Port{{"one", 1}, {"two", 2}, {"three", 3}},
   136  											DynamicPorts:  []structs.Port{{"http", 0}, {"https", 0}, {"admin", 0}},
   137  										},
   138  									},
   139  								},
   140  								KillTimeout: 22 * time.Second,
   141  								LogConfig: &structs.LogConfig{
   142  									MaxFiles:      10,
   143  									MaxFileSizeMB: 100,
   144  								},
   145  								Artifacts: []*structs.TaskArtifact{
   146  									{
   147  										GetterSource: "http://foo.com/artifact",
   148  										RelativeDest: "local/",
   149  										GetterOptions: map[string]string{
   150  											"checksum": "md5:b8a4f3f72ecab0510a6a31e997461c5f",
   151  										},
   152  									},
   153  									{
   154  										GetterSource: "http://bar.com/artifact",
   155  										RelativeDest: "local/",
   156  										GetterOptions: map[string]string{
   157  											"checksum": "md5:ff1cc0d3432dad54d607c1505fb7245c",
   158  										},
   159  									},
   160  								},
   161  								Vault: &structs.Vault{
   162  									Policies:   []string{"foo", "bar"},
   163  									Env:        true,
   164  									ChangeMode: structs.VaultChangeModeRestart,
   165  								},
   166  								Templates: []*structs.Template{
   167  									{
   168  										SourcePath:   "foo",
   169  										DestPath:     "foo",
   170  										ChangeMode:   "foo",
   171  										ChangeSignal: "foo",
   172  										Splay:        10 * time.Second,
   173  									},
   174  									{
   175  										SourcePath:   "bar",
   176  										DestPath:     "bar",
   177  										ChangeMode:   structs.TemplateChangeModeRestart,
   178  										ChangeSignal: "",
   179  										Splay:        5 * time.Second,
   180  									},
   181  								},
   182  							},
   183  							&structs.Task{
   184  								Name:   "storagelocker",
   185  								Driver: "docker",
   186  								User:   "",
   187  								Config: map[string]interface{}{
   188  									"image": "hashicorp/storagelocker",
   189  								},
   190  								Resources: &structs.Resources{
   191  									CPU:      500,
   192  									MemoryMB: 128,
   193  									IOPS:     30,
   194  								},
   195  								Constraints: []*structs.Constraint{
   196  									&structs.Constraint{
   197  										LTarget: "kernel.arch",
   198  										RTarget: "amd64",
   199  										Operand: "=",
   200  									},
   201  								},
   202  								LogConfig: structs.DefaultLogConfig(),
   203  								Vault: &structs.Vault{
   204  									Policies:     []string{"foo", "bar"},
   205  									Env:          false,
   206  									ChangeMode:   structs.VaultChangeModeSignal,
   207  									ChangeSignal: "SIGUSR1",
   208  								},
   209  							},
   210  						},
   211  					},
   212  				},
   213  			},
   214  			false,
   215  		},
   216  
   217  		{
   218  			"multi-network.hcl",
   219  			nil,
   220  			true,
   221  		},
   222  
   223  		{
   224  			"multi-resource.hcl",
   225  			nil,
   226  			true,
   227  		},
   228  
   229  		{
   230  			"multi-vault.hcl",
   231  			nil,
   232  			true,
   233  		},
   234  
   235  		{
   236  			"default-job.hcl",
   237  			&structs.Job{
   238  				ID:       "foo",
   239  				Name:     "foo",
   240  				Priority: 50,
   241  				Region:   "global",
   242  				Type:     "service",
   243  			},
   244  			false,
   245  		},
   246  
   247  		{
   248  			"version-constraint.hcl",
   249  			&structs.Job{
   250  				ID:       "foo",
   251  				Name:     "foo",
   252  				Priority: 50,
   253  				Region:   "global",
   254  				Type:     "service",
   255  				Constraints: []*structs.Constraint{
   256  					&structs.Constraint{
   257  						LTarget: "$attr.kernel.version",
   258  						RTarget: "~> 3.2",
   259  						Operand: structs.ConstraintVersion,
   260  					},
   261  				},
   262  			},
   263  			false,
   264  		},
   265  
   266  		{
   267  			"regexp-constraint.hcl",
   268  			&structs.Job{
   269  				ID:       "foo",
   270  				Name:     "foo",
   271  				Priority: 50,
   272  				Region:   "global",
   273  				Type:     "service",
   274  				Constraints: []*structs.Constraint{
   275  					&structs.Constraint{
   276  						LTarget: "$attr.kernel.version",
   277  						RTarget: "[0-9.]+",
   278  						Operand: structs.ConstraintRegex,
   279  					},
   280  				},
   281  			},
   282  			false,
   283  		},
   284  
   285  		{
   286  			"set-contains-constraint.hcl",
   287  			&structs.Job{
   288  				ID:       "foo",
   289  				Name:     "foo",
   290  				Priority: 50,
   291  				Region:   "global",
   292  				Type:     "service",
   293  				Constraints: []*structs.Constraint{
   294  					&structs.Constraint{
   295  						LTarget: "$meta.data",
   296  						RTarget: "foo,bar,baz",
   297  						Operand: structs.ConstraintSetContains,
   298  					},
   299  				},
   300  			},
   301  			false,
   302  		},
   303  
   304  		{
   305  			"distinctHosts-constraint.hcl",
   306  			&structs.Job{
   307  				ID:       "foo",
   308  				Name:     "foo",
   309  				Priority: 50,
   310  				Region:   "global",
   311  				Type:     "service",
   312  				Constraints: []*structs.Constraint{
   313  					&structs.Constraint{
   314  						Operand: structs.ConstraintDistinctHosts,
   315  					},
   316  				},
   317  			},
   318  			false,
   319  		},
   320  
   321  		{
   322  			"periodic-cron.hcl",
   323  			&structs.Job{
   324  				ID:       "foo",
   325  				Name:     "foo",
   326  				Priority: 50,
   327  				Region:   "global",
   328  				Type:     "service",
   329  				Periodic: &structs.PeriodicConfig{
   330  					Enabled:         true,
   331  					SpecType:        structs.PeriodicSpecCron,
   332  					Spec:            "*/5 * * *",
   333  					ProhibitOverlap: true,
   334  				},
   335  			},
   336  			false,
   337  		},
   338  
   339  		{
   340  			"specify-job.hcl",
   341  			&structs.Job{
   342  				ID:       "job1",
   343  				Name:     "My Job",
   344  				Priority: 50,
   345  				Region:   "global",
   346  				Type:     "service",
   347  			},
   348  			false,
   349  		},
   350  
   351  		{
   352  			"task-nested-config.hcl",
   353  			&structs.Job{
   354  				Region:   "global",
   355  				ID:       "foo",
   356  				Name:     "foo",
   357  				Type:     "service",
   358  				Priority: 50,
   359  
   360  				TaskGroups: []*structs.TaskGroup{
   361  					&structs.TaskGroup{
   362  						Name:          "bar",
   363  						Count:         1,
   364  						EphemeralDisk: structs.DefaultEphemeralDisk(),
   365  						Tasks: []*structs.Task{
   366  							&structs.Task{
   367  								Name:   "bar",
   368  								Driver: "docker",
   369  								Config: map[string]interface{}{
   370  									"image": "hashicorp/image",
   371  									"port_map": []map[string]interface{}{
   372  										map[string]interface{}{
   373  											"db": 1234,
   374  										},
   375  									},
   376  								},
   377  								LogConfig: &structs.LogConfig{
   378  									MaxFiles:      10,
   379  									MaxFileSizeMB: 10,
   380  								},
   381  							},
   382  						},
   383  					},
   384  				},
   385  			},
   386  			false,
   387  		},
   388  
   389  		{
   390  			"bad-artifact.hcl",
   391  			nil,
   392  			true,
   393  		},
   394  
   395  		{
   396  			"artifacts.hcl",
   397  			&structs.Job{
   398  				ID:       "binstore-storagelocker",
   399  				Name:     "binstore-storagelocker",
   400  				Type:     "service",
   401  				Priority: 50,
   402  				Region:   "global",
   403  
   404  				TaskGroups: []*structs.TaskGroup{
   405  					&structs.TaskGroup{
   406  						Name:          "binsl",
   407  						Count:         1,
   408  						EphemeralDisk: structs.DefaultEphemeralDisk(),
   409  						Tasks: []*structs.Task{
   410  							&structs.Task{
   411  								Name:   "binstore",
   412  								Driver: "docker",
   413  								Resources: &structs.Resources{
   414  									CPU:      100,
   415  									MemoryMB: 10,
   416  									IOPS:     0,
   417  								},
   418  								LogConfig: &structs.LogConfig{
   419  									MaxFiles:      10,
   420  									MaxFileSizeMB: 10,
   421  								},
   422  								Artifacts: []*structs.TaskArtifact{
   423  									{
   424  										GetterSource:  "http://foo.com/bar",
   425  										GetterOptions: map[string]string{"foo": "bar"},
   426  										RelativeDest:  "",
   427  									},
   428  									{
   429  										GetterSource:  "http://foo.com/baz",
   430  										GetterOptions: nil,
   431  										RelativeDest:  "local/",
   432  									},
   433  									{
   434  										GetterSource:  "http://foo.com/bam",
   435  										GetterOptions: nil,
   436  										RelativeDest:  "var/foo",
   437  									},
   438  								},
   439  							},
   440  						},
   441  					},
   442  				},
   443  			},
   444  			false,
   445  		},
   446  		{
   447  			"service-check-initial-status.hcl",
   448  			&structs.Job{
   449  				ID:       "check_initial_status",
   450  				Name:     "check_initial_status",
   451  				Type:     "service",
   452  				Priority: 50,
   453  				Region:   "global",
   454  				TaskGroups: []*structs.TaskGroup{
   455  					&structs.TaskGroup{
   456  						Name:          "group",
   457  						Count:         1,
   458  						EphemeralDisk: structs.DefaultEphemeralDisk(),
   459  						Tasks: []*structs.Task{
   460  							&structs.Task{
   461  								Name: "task",
   462  								Services: []*structs.Service{
   463  									{
   464  										Name:      "check_initial_status-group-task",
   465  										Tags:      []string{"foo", "bar"},
   466  										PortLabel: "http",
   467  										Checks: []*structs.ServiceCheck{
   468  											{
   469  												Name:          "check-name",
   470  												Type:          "http",
   471  												Interval:      10 * time.Second,
   472  												Timeout:       2 * time.Second,
   473  												InitialStatus: api.HealthPassing,
   474  											},
   475  										},
   476  									},
   477  								},
   478  								LogConfig: structs.DefaultLogConfig(),
   479  							},
   480  						},
   481  					},
   482  				},
   483  			},
   484  			false,
   485  		},
   486  		{
   487  			"vault_inheritance.hcl",
   488  			&structs.Job{
   489  				ID:       "example",
   490  				Name:     "example",
   491  				Type:     "service",
   492  				Priority: 50,
   493  				Region:   "global",
   494  				TaskGroups: []*structs.TaskGroup{
   495  					&structs.TaskGroup{
   496  						Name:          "cache",
   497  						Count:         1,
   498  						EphemeralDisk: structs.DefaultEphemeralDisk(),
   499  						Tasks: []*structs.Task{
   500  							&structs.Task{
   501  								Name:      "redis",
   502  								LogConfig: structs.DefaultLogConfig(),
   503  								Vault: &structs.Vault{
   504  									Policies:   []string{"group"},
   505  									Env:        true,
   506  									ChangeMode: structs.VaultChangeModeRestart,
   507  								},
   508  							},
   509  							&structs.Task{
   510  								Name:      "redis2",
   511  								LogConfig: structs.DefaultLogConfig(),
   512  								Vault: &structs.Vault{
   513  									Policies:   []string{"task"},
   514  									Env:        false,
   515  									ChangeMode: structs.VaultChangeModeRestart,
   516  								},
   517  							},
   518  						},
   519  					},
   520  					&structs.TaskGroup{
   521  						Name:          "cache2",
   522  						Count:         1,
   523  						EphemeralDisk: structs.DefaultEphemeralDisk(),
   524  						Tasks: []*structs.Task{
   525  							&structs.Task{
   526  								Name:      "redis",
   527  								LogConfig: structs.DefaultLogConfig(),
   528  								Vault: &structs.Vault{
   529  									Policies:   []string{"job"},
   530  									Env:        true,
   531  									ChangeMode: structs.VaultChangeModeRestart,
   532  								},
   533  							},
   534  						},
   535  					},
   536  				},
   537  			},
   538  			false,
   539  		},
   540  	}
   541  
   542  	for _, tc := range cases {
   543  		t.Logf("Testing parse: %s", tc.File)
   544  
   545  		path, err := filepath.Abs(filepath.Join("./test-fixtures", tc.File))
   546  		if err != nil {
   547  			t.Fatalf("file: %s\n\n%s", tc.File, err)
   548  			continue
   549  		}
   550  
   551  		actual, err := ParseFile(path)
   552  		if (err != nil) != tc.Err {
   553  			t.Fatalf("file: %s\n\n%s", tc.File, err)
   554  			continue
   555  		}
   556  
   557  		if !reflect.DeepEqual(actual, tc.Result) {
   558  			diff, err := actual.Diff(tc.Result, true)
   559  			if err == nil {
   560  				t.Logf("file %s diff:\n%#v\n", tc.File, diff)
   561  			}
   562  			t.Fatalf("file: %s\n\n%#v\n\n%#v", tc.File, actual, tc.Result)
   563  		}
   564  	}
   565  }
   566  
   567  func TestBadConfigEmpty(t *testing.T) {
   568  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config-empty.hcl"))
   569  	if err != nil {
   570  		t.Fatalf("Can't get absolute path for file: %s", err)
   571  	}
   572  
   573  	_, err = ParseFile(path)
   574  
   575  	if !strings.Contains(err.Error(), "field \"image\" is required, but no value was found") {
   576  		t.Fatalf("\nExpected error\n  %s\ngot\n  %v",
   577  			"field \"image\" is required, but no value was found",
   578  			err,
   579  		)
   580  	}
   581  }
   582  
   583  func TestBadConfigMissing(t *testing.T) {
   584  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config-missing.hcl"))
   585  	if err != nil {
   586  		t.Fatalf("Can't get absolute path for file: %s", err)
   587  	}
   588  
   589  	_, err = ParseFile(path)
   590  
   591  	if !strings.Contains(err.Error(), "field \"image\" is required") {
   592  		t.Fatalf("\nExpected error\n  %s\ngot\n  %v",
   593  			"field \"image\" is required",
   594  			err,
   595  		)
   596  	}
   597  }
   598  
   599  func TestBadConfig(t *testing.T) {
   600  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config.hcl"))
   601  	if err != nil {
   602  		t.Fatalf("Can't get absolute path for file: %s", err)
   603  	}
   604  
   605  	_, err = ParseFile(path)
   606  
   607  	if !strings.Contains(err.Error(), "seem to be of type boolean") {
   608  		t.Fatalf("\nExpected error\n  %s\ngot\n  %v",
   609  			"seem to be of type boolean",
   610  			err,
   611  		)
   612  	}
   613  
   614  	if !strings.Contains(err.Error(), "\"foo\" is an invalid field") {
   615  		t.Fatalf("\nExpected error\n  %s\ngot\n  %v",
   616  			"\"foo\" is an invalid field",
   617  			err,
   618  		)
   619  	}
   620  }
   621  
   622  func TestBadPorts(t *testing.T) {
   623  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-ports.hcl"))
   624  	if err != nil {
   625  		t.Fatalf("Can't get absolute path for file: %s", err)
   626  	}
   627  
   628  	_, err = ParseFile(path)
   629  
   630  	if !strings.Contains(err.Error(), errPortLabel.Error()) {
   631  		t.Fatalf("\nExpected error\n  %s\ngot\n  %v", errPortLabel, err)
   632  	}
   633  }
   634  
   635  func TestOverlappingPorts(t *testing.T) {
   636  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "overlapping-ports.hcl"))
   637  	if err != nil {
   638  		t.Fatalf("Can't get absolute path for file: %s", err)
   639  	}
   640  
   641  	_, err = ParseFile(path)
   642  
   643  	if err == nil {
   644  		t.Fatalf("Expected an error")
   645  	}
   646  
   647  	if !strings.Contains(err.Error(), "found a port label collision") {
   648  		t.Fatalf("Expected collision error; got %v", err)
   649  	}
   650  }
   651  
   652  func TestIncompleteServiceDefn(t *testing.T) {
   653  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "incorrect-service-def.hcl"))
   654  	if err != nil {
   655  		t.Fatalf("Can't get absolute path for file: %s", err)
   656  	}
   657  
   658  	_, err = ParseFile(path)
   659  
   660  	if err == nil {
   661  		t.Fatalf("Expected an error")
   662  	}
   663  
   664  	if !strings.Contains(err.Error(), "Only one service block may omit the Name field") {
   665  		t.Fatalf("Expected collision error; got %v", err)
   666  	}
   667  }
   668  
   669  func TestIncorrectKey(t *testing.T) {
   670  	path, err := filepath.Abs(filepath.Join("./test-fixtures", "basic_wrong_key.hcl"))
   671  	if err != nil {
   672  		t.Fatalf("Can't get absolute path for file: %s", err)
   673  	}
   674  
   675  	_, err = ParseFile(path)
   676  
   677  	if err == nil {
   678  		t.Fatalf("Expected an error")
   679  	}
   680  
   681  	if !strings.Contains(err.Error(), "* group: 'binsl', task: 'binstore', service: 'binstore-storagelocker-binsl-binstore', check -> invalid key: nterval") {
   682  		t.Fatalf("Expected collision error; got %v", err)
   683  	}
   684  }