github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/envoy_version_hook_test.go (about)

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