1package taskrunner
2
3import (
4	"context"
5	"testing"
6
7	"github.com/hashicorp/nomad/client/allocdir"
8	ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
9	"github.com/hashicorp/nomad/client/taskenv"
10	"github.com/hashicorp/nomad/command/agent/consul"
11	"github.com/hashicorp/nomad/helper/envoy"
12	"github.com/hashicorp/nomad/helper/testlog"
13	"github.com/hashicorp/nomad/nomad/mock"
14	"github.com/hashicorp/nomad/nomad/structs"
15	"github.com/pkg/errors"
16	"github.com/stretchr/testify/require"
17)
18
19var (
20	taskEnvDefault = taskenv.NewTaskEnv(nil, nil, nil, map[string]string{
21		"meta.connect.sidecar_image": envoy.ImageFormat,
22		"meta.connect.gateway_image": envoy.ImageFormat,
23	}, "", "")
24)
25
26func TestEnvoyVersionHook_semver(t *testing.T) {
27	t.Parallel()
28
29	t.Run("with v", func(t *testing.T) {
30		result, err := semver("v1.2.3")
31		require.NoError(t, err)
32		require.Equal(t, "1.2.3", result)
33	})
34
35	t.Run("without v", func(t *testing.T) {
36		result, err := semver("1.2.3")
37		require.NoError(t, err)
38		require.Equal(t, "1.2.3", result)
39	})
40
41	t.Run("unexpected", func(t *testing.T) {
42		_, err := semver("foo")
43		require.EqualError(t, err, "unexpected envoy version format: Malformed version: foo")
44	})
45}
46
47func TestEnvoyVersionHook_taskImage(t *testing.T) {
48	t.Parallel()
49
50	t.Run("absent", func(t *testing.T) {
51		result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{
52			// empty
53		})
54		require.Equal(t, envoy.ImageFormat, result)
55	})
56
57	t.Run("not a string", func(t *testing.T) {
58		result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{
59			"image": 7, // not a string
60		})
61		require.Equal(t, envoy.ImageFormat, result)
62	})
63
64	t.Run("normal", func(t *testing.T) {
65		result := (*envoyVersionHook)(nil).taskImage(map[string]interface{}{
66			"image": "custom/envoy:latest",
67		})
68		require.Equal(t, "custom/envoy:latest", result)
69	})
70}
71
72func TestEnvoyVersionHook_tweakImage(t *testing.T) {
73	t.Parallel()
74
75	image := envoy.ImageFormat
76
77	t.Run("legacy", func(t *testing.T) {
78		result, err := (*envoyVersionHook)(nil).tweakImage(image, nil)
79		require.NoError(t, err)
80		require.Equal(t, envoy.FallbackImage, result)
81	})
82
83	t.Run("unexpected", func(t *testing.T) {
84		_, err := (*envoyVersionHook)(nil).tweakImage(image, map[string][]string{
85			"envoy": {"foo", "bar", "baz"},
86		})
87		require.EqualError(t, err, "unexpected envoy version format: Malformed version: foo")
88	})
89
90	t.Run("standard envoy", func(t *testing.T) {
91		result, err := (*envoyVersionHook)(nil).tweakImage(image, map[string][]string{
92			"envoy": {"1.15.0", "1.14.4", "1.13.4", "1.12.6"},
93		})
94		require.NoError(t, err)
95		require.Equal(t, "envoyproxy/envoy:v1.15.0", result)
96	})
97
98	t.Run("custom image", func(t *testing.T) {
99		custom := "custom-${NOMAD_envoy_version}/envoy:${NOMAD_envoy_version}"
100		result, err := (*envoyVersionHook)(nil).tweakImage(custom, map[string][]string{
101			"envoy": {"1.15.0", "1.14.4", "1.13.4", "1.12.6"},
102		})
103		require.NoError(t, err)
104		require.Equal(t, "custom-1.15.0/envoy:1.15.0", result)
105	})
106}
107
108func TestEnvoyVersionHook_interpolateImage(t *testing.T) {
109	t.Parallel()
110
111	hook := (*envoyVersionHook)(nil)
112
113	t.Run("default sidecar", func(t *testing.T) {
114		task := &structs.Task{
115			Config: map[string]interface{}{"image": envoy.SidecarConfigVar},
116		}
117		hook.interpolateImage(task, taskEnvDefault)
118		require.Equal(t, envoy.ImageFormat, task.Config["image"])
119	})
120
121	t.Run("default gateway", func(t *testing.T) {
122		task := &structs.Task{
123			Config: map[string]interface{}{"image": envoy.GatewayConfigVar},
124		}
125		hook.interpolateImage(task, taskEnvDefault)
126		require.Equal(t, envoy.ImageFormat, task.Config["image"])
127	})
128
129	t.Run("custom static", func(t *testing.T) {
130		task := &structs.Task{
131			Config: map[string]interface{}{"image": "custom/envoy"},
132		}
133		hook.interpolateImage(task, taskEnvDefault)
134		require.Equal(t, "custom/envoy", task.Config["image"])
135	})
136
137	t.Run("custom interpolated", func(t *testing.T) {
138		task := &structs.Task{
139			Config: map[string]interface{}{"image": "${MY_ENVOY}"},
140		}
141		hook.interpolateImage(task, taskenv.NewTaskEnv(map[string]string{
142			"MY_ENVOY": "my/envoy",
143		}, map[string]string{
144			"MY_ENVOY": "my/envoy",
145		}, nil, nil, "", ""))
146		require.Equal(t, "my/envoy", task.Config["image"])
147	})
148
149	t.Run("no image", func(t *testing.T) {
150		task := &structs.Task{
151			Config: map[string]interface{}{},
152		}
153		hook.interpolateImage(task, taskEnvDefault)
154		require.Empty(t, task.Config)
155	})
156}
157
158func TestEnvoyVersionHook_skip(t *testing.T) {
159	t.Parallel()
160
161	h := new(envoyVersionHook)
162
163	t.Run("not docker", func(t *testing.T) {
164		skip := h.skip(&ifs.TaskPrestartRequest{
165			Task: &structs.Task{
166				Driver: "exec",
167				Config: nil,
168			},
169		})
170		require.True(t, skip)
171	})
172
173	t.Run("not connect", func(t *testing.T) {
174		skip := h.skip(&ifs.TaskPrestartRequest{
175			Task: &structs.Task{
176				Driver: "docker",
177				Kind:   "",
178			},
179		})
180		require.True(t, skip)
181	})
182
183	t.Run("version not needed", func(t *testing.T) {
184		skip := h.skip(&ifs.TaskPrestartRequest{
185			Task: &structs.Task{
186				Driver: "docker",
187				Kind:   structs.NewTaskKind(structs.ConnectProxyPrefix, "task"),
188				Config: map[string]interface{}{
189					"image": "custom/envoy:latest",
190				},
191			},
192		})
193		require.True(t, skip)
194	})
195
196	t.Run("version needed custom", func(t *testing.T) {
197		skip := h.skip(&ifs.TaskPrestartRequest{
198			Task: &structs.Task{
199				Driver: "docker",
200				Kind:   structs.NewTaskKind(structs.ConnectProxyPrefix, "task"),
201				Config: map[string]interface{}{
202					"image": "custom/envoy:v${NOMAD_envoy_version}",
203				},
204			},
205		})
206		require.False(t, skip)
207	})
208
209	t.Run("version needed standard", func(t *testing.T) {
210		skip := h.skip(&ifs.TaskPrestartRequest{
211			Task: &structs.Task{
212				Driver: "docker",
213				Kind:   structs.NewTaskKind(structs.ConnectProxyPrefix, "task"),
214				Config: map[string]interface{}{
215					"image": envoy.ImageFormat,
216				},
217			},
218		})
219		require.False(t, skip)
220	})
221}
222
223func TestTaskRunner_EnvoyVersionHook_Prestart_standard(t *testing.T) {
224	t.Parallel()
225
226	logger := testlog.HCLogger(t)
227
228	// Setup an Allocation
229	alloc := mock.ConnectAlloc()
230	alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask()
231	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook")
232	defer cleanupDir()
233
234	// Setup a mock for Consul API
235	spAPI := consul.MockSupportedProxiesAPI{
236		Value: map[string][]string{
237			"envoy": {"1.15.0", "1.14.4"},
238		},
239		Error: nil,
240	}
241
242	// Run envoy_version hook
243	h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger))
244
245	// Create a prestart request
246	request := &ifs.TaskPrestartRequest{
247		Task:    alloc.Job.TaskGroups[0].Tasks[0],
248		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
249		TaskEnv: taskEnvDefault,
250	}
251	require.NoError(t, request.TaskDir.Build(false, nil))
252
253	// Prepare a response
254	var response ifs.TaskPrestartResponse
255
256	// Run the hook
257	require.NoError(t, h.Prestart(context.Background(), request, &response))
258
259	// Assert the hook is Done
260	require.True(t, response.Done)
261
262	// Assert the Task.Config[image] is concrete
263	require.Equal(t, "envoyproxy/envoy:v1.15.0", request.Task.Config["image"])
264}
265
266func TestTaskRunner_EnvoyVersionHook_Prestart_custom(t *testing.T) {
267	t.Parallel()
268
269	logger := testlog.HCLogger(t)
270
271	// Setup an Allocation
272	alloc := mock.ConnectAlloc()
273	alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask()
274	alloc.Job.TaskGroups[0].Tasks[0].Config["image"] = "custom-${NOMAD_envoy_version}:latest"
275	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook")
276	defer cleanupDir()
277
278	// Setup a mock for Consul API
279	spAPI := consul.MockSupportedProxiesAPI{
280		Value: map[string][]string{
281			"envoy": {"1.14.1", "1.13.3"},
282		},
283		Error: nil,
284	}
285
286	// Run envoy_version hook
287	h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger))
288
289	// Create a prestart request
290	request := &ifs.TaskPrestartRequest{
291		Task:    alloc.Job.TaskGroups[0].Tasks[0],
292		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
293		TaskEnv: taskEnvDefault,
294	}
295	require.NoError(t, request.TaskDir.Build(false, nil))
296
297	// Prepare a response
298	var response ifs.TaskPrestartResponse
299
300	// Run the hook
301	require.NoError(t, h.Prestart(context.Background(), request, &response))
302
303	// Assert the hook is Done
304	require.True(t, response.Done)
305
306	// Assert the Task.Config[image] is concrete
307	require.Equal(t, "custom-1.14.1:latest", request.Task.Config["image"])
308}
309
310func TestTaskRunner_EnvoyVersionHook_Prestart_skip(t *testing.T) {
311	t.Parallel()
312
313	logger := testlog.HCLogger(t)
314
315	// Setup an Allocation
316	alloc := mock.ConnectAlloc()
317	alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask()
318	alloc.Job.TaskGroups[0].Tasks[0].Driver = "exec"
319	alloc.Job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
320		"command": "/sidecar",
321	}
322	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook")
323	defer cleanupDir()
324
325	// Setup a mock for Consul API
326	spAPI := consul.MockSupportedProxiesAPI{
327		Value: map[string][]string{
328			"envoy": {"1.14.1", "1.13.3"},
329		},
330		Error: nil,
331	}
332
333	// Run envoy_version hook
334	h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger))
335
336	// Create a prestart request
337	request := &ifs.TaskPrestartRequest{
338		Task:    alloc.Job.TaskGroups[0].Tasks[0],
339		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
340		TaskEnv: taskEnvDefault,
341	}
342	require.NoError(t, request.TaskDir.Build(false, nil))
343
344	// Prepare a response
345	var response ifs.TaskPrestartResponse
346
347	// Run the hook
348	require.NoError(t, h.Prestart(context.Background(), request, &response))
349
350	// Assert the hook is Done
351	require.True(t, response.Done)
352
353	// Assert the Task.Config[image] does not get set
354	require.Empty(t, request.Task.Config["image"])
355}
356
357func TestTaskRunner_EnvoyVersionHook_Prestart_fallback(t *testing.T) {
358	t.Parallel()
359
360	logger := testlog.HCLogger(t)
361
362	// Setup an Allocation
363	alloc := mock.ConnectAlloc()
364	alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask()
365	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook")
366	defer cleanupDir()
367
368	// Setup a mock for Consul API
369	spAPI := consul.MockSupportedProxiesAPI{
370		Value: nil, // old consul, no .xDS.SupportedProxies
371		Error: nil,
372	}
373
374	// Run envoy_version hook
375	h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger))
376
377	// Create a prestart request
378	request := &ifs.TaskPrestartRequest{
379		Task:    alloc.Job.TaskGroups[0].Tasks[0],
380		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
381		TaskEnv: taskEnvDefault,
382	}
383	require.NoError(t, request.TaskDir.Build(false, nil))
384
385	// Prepare a response
386	var response ifs.TaskPrestartResponse
387
388	// Run the hook
389	require.NoError(t, h.Prestart(context.Background(), request, &response))
390
391	// Assert the hook is Done
392	require.True(t, response.Done)
393
394	// Assert the Task.Config[image] is the fallback image
395	require.Equal(t, "envoyproxy/envoy:v1.11.2@sha256:a7769160c9c1a55bb8d07a3b71ce5d64f72b1f665f10d81aa1581bc3cf850d09", request.Task.Config["image"])
396}
397
398func TestTaskRunner_EnvoyVersionHook_Prestart_error(t *testing.T) {
399	t.Parallel()
400
401	logger := testlog.HCLogger(t)
402
403	// Setup an Allocation
404	alloc := mock.ConnectAlloc()
405	alloc.Job.TaskGroups[0].Tasks[0] = mock.ConnectSidecarTask()
406	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyVersionHook")
407	defer cleanupDir()
408
409	// Setup a mock for Consul API
410	spAPI := consul.MockSupportedProxiesAPI{
411		Value: nil,
412		Error: errors.New("some consul error"),
413	}
414
415	// Run envoy_version hook
416	h := newEnvoyVersionHook(newEnvoyVersionHookConfig(alloc, spAPI, logger))
417
418	// Create a prestart request
419	request := &ifs.TaskPrestartRequest{
420		Task:    alloc.Job.TaskGroups[0].Tasks[0],
421		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
422		TaskEnv: taskEnvDefault,
423	}
424	require.NoError(t, request.TaskDir.Build(false, nil))
425
426	// Prepare a response
427	var response ifs.TaskPrestartResponse
428
429	// Run the hook, error should be recoverable
430	err := h.Prestart(context.Background(), request, &response)
431	require.EqualError(t, err, "error retrieving supported Envoy versions from Consul: some consul error")
432
433	// Assert the hook is not Done
434	require.False(t, response.Done)
435}
436