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

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"testing"
    13  
    14  	"github.com/hashicorp/nomad/ci"
    15  	"github.com/hashicorp/nomad/client/allocdir"
    16  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    17  	"github.com/hashicorp/nomad/client/allocrunner/taskrunner/getter"
    18  	"github.com/hashicorp/nomad/client/taskenv"
    19  	"github.com/hashicorp/nomad/client/testutil"
    20  	"github.com/hashicorp/nomad/helper/testlog"
    21  	"github.com/hashicorp/nomad/nomad/structs"
    22  	"github.com/stretchr/testify/require"
    23  	"golang.org/x/exp/maps"
    24  )
    25  
    26  // Statically assert the artifact hook implements the expected interface
    27  var _ interfaces.TaskPrestartHook = (*artifactHook)(nil)
    28  
    29  type mockEmitter struct {
    30  	events []*structs.TaskEvent
    31  }
    32  
    33  func (m *mockEmitter) EmitEvent(ev *structs.TaskEvent) {
    34  	m.events = append(m.events, ev)
    35  }
    36  
    37  // TestTaskRunner_ArtifactHook_Recoverable asserts that failures to download
    38  // artifacts are a recoverable error.
    39  func TestTaskRunner_ArtifactHook_Recoverable(t *testing.T) {
    40  	ci.Parallel(t)
    41  
    42  	me := &mockEmitter{}
    43  	sbox := getter.TestSandbox(t)
    44  	artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t))
    45  
    46  	req := &interfaces.TaskPrestartRequest{
    47  		TaskEnv: taskenv.NewEmptyTaskEnv(),
    48  		TaskDir: &allocdir.TaskDir{Dir: os.TempDir()},
    49  		Task: &structs.Task{
    50  			Artifacts: []*structs.TaskArtifact{
    51  				{
    52  					GetterSource: "http://127.0.0.1:0",
    53  					GetterMode:   structs.GetterModeAny,
    54  				},
    55  			},
    56  		},
    57  	}
    58  
    59  	resp := interfaces.TaskPrestartResponse{}
    60  
    61  	err := artifactHook.Prestart(context.Background(), req, &resp)
    62  
    63  	require.False(t, resp.Done)
    64  	require.NotNil(t, err)
    65  	require.True(t, structs.IsRecoverable(err))
    66  	require.Len(t, me.events, 1)
    67  	require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type)
    68  }
    69  
    70  // TestTaskRunnerArtifactHook_PartialDone asserts that the artifact hook skips
    71  // already downloaded artifacts when subsequent artifacts fail and cause a
    72  // restart.
    73  func TestTaskRunner_ArtifactHook_PartialDone(t *testing.T) {
    74  	testutil.RequireRoot(t)
    75  	ci.Parallel(t)
    76  
    77  	me := &mockEmitter{}
    78  	sbox := getter.TestSandbox(t)
    79  	artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t))
    80  
    81  	// Create a source directory with 1 of the 2 artifacts
    82  	srcdir := t.TempDir()
    83  
    84  	// Only create one of the 2 artifacts to cause an error on first run.
    85  	file1 := filepath.Join(srcdir, "foo.txt")
    86  	require.NoError(t, ioutil.WriteFile(file1, []byte{'1'}, 0644))
    87  
    88  	// Test server to serve the artifacts
    89  	ts := httptest.NewServer(http.FileServer(http.Dir(srcdir)))
    90  	defer ts.Close()
    91  
    92  	// Create the target directory.
    93  	_, destdir := getter.SetupDir(t)
    94  
    95  	req := &interfaces.TaskPrestartRequest{
    96  		TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""),
    97  		TaskDir: &allocdir.TaskDir{Dir: destdir},
    98  		Task: &structs.Task{
    99  			Artifacts: []*structs.TaskArtifact{
   100  				{
   101  					GetterSource: ts.URL + "/foo.txt",
   102  					GetterMode:   structs.GetterModeAny,
   103  				},
   104  				{
   105  					GetterSource: ts.URL + "/bar.txt",
   106  					GetterMode:   structs.GetterModeAny,
   107  				},
   108  			},
   109  		},
   110  	}
   111  
   112  	resp := interfaces.TaskPrestartResponse{}
   113  
   114  	// On first run file1 (foo) should download but file2 (bar) should
   115  	// fail.
   116  	err := artifactHook.Prestart(context.Background(), req, &resp)
   117  
   118  	require.NotNil(t, err)
   119  	require.True(t, structs.IsRecoverable(err))
   120  	require.Len(t, resp.State, 1)
   121  	require.False(t, resp.Done)
   122  	require.Len(t, me.events, 1)
   123  	require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type)
   124  
   125  	// Remove file1 from the server so it errors if its downloaded again.
   126  	require.NoError(t, os.Remove(file1))
   127  
   128  	// Write file2 so artifacts can download successfully
   129  	file2 := filepath.Join(srcdir, "bar.txt")
   130  	require.NoError(t, ioutil.WriteFile(file2, []byte{'1'}, 0644))
   131  
   132  	// Mock TaskRunner by copying state from resp to req and reset resp.
   133  	req.PreviousState = maps.Clone(resp.State)
   134  
   135  	resp = interfaces.TaskPrestartResponse{}
   136  
   137  	// Retry the download and assert it succeeds
   138  	err = artifactHook.Prestart(context.Background(), req, &resp)
   139  
   140  	require.NoError(t, err)
   141  	require.True(t, resp.Done)
   142  	require.Len(t, resp.State, 2)
   143  
   144  	// Assert both files downloaded properly
   145  	files, err := filepath.Glob(filepath.Join(destdir, "*.txt"))
   146  	require.NoError(t, err)
   147  	sort.Strings(files)
   148  	require.Contains(t, files[0], "bar.txt")
   149  	require.Contains(t, files[1], "foo.txt")
   150  
   151  	// Stop the test server entirely and assert that re-running works
   152  	ts.Close()
   153  	req.PreviousState = maps.Clone(resp.State)
   154  	resp = interfaces.TaskPrestartResponse{}
   155  	err = artifactHook.Prestart(context.Background(), req, &resp)
   156  	require.NoError(t, err)
   157  	require.True(t, resp.Done)
   158  	require.Len(t, resp.State, 2)
   159  }
   160  
   161  // TestTaskRunner_ArtifactHook_ConcurrentDownloadSuccess asserts that the artifact hook
   162  // download multiple files concurrently. this is a successful test without any errors.
   163  func TestTaskRunner_ArtifactHook_ConcurrentDownloadSuccess(t *testing.T) {
   164  	t.Parallel()
   165  
   166  	me := &mockEmitter{}
   167  	sbox := getter.TestSandbox(t)
   168  	artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t))
   169  
   170  	// Create a source directory all 7 artifacts
   171  	srcdir := t.TempDir()
   172  
   173  	numOfFiles := 7
   174  	for i := 0; i < numOfFiles; i++ {
   175  		file := filepath.Join(srcdir, fmt.Sprintf("file%d.txt", i))
   176  		require.NoError(t, ioutil.WriteFile(file, []byte{byte(i)}, 0644))
   177  	}
   178  
   179  	// Test server to serve the artifacts
   180  	ts := httptest.NewServer(http.FileServer(http.Dir(srcdir)))
   181  	defer ts.Close()
   182  
   183  	// Create the target directory.
   184  	_, destdir := getter.SetupDir(t)
   185  
   186  	req := &interfaces.TaskPrestartRequest{
   187  		TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""),
   188  		TaskDir: &allocdir.TaskDir{Dir: destdir},
   189  		Task: &structs.Task{
   190  			Artifacts: []*structs.TaskArtifact{
   191  				{
   192  					GetterSource: ts.URL + "/file0.txt",
   193  					GetterMode:   structs.GetterModeAny,
   194  				},
   195  				{
   196  					GetterSource: ts.URL + "/file1.txt",
   197  					GetterMode:   structs.GetterModeAny,
   198  				},
   199  				{
   200  					GetterSource: ts.URL + "/file2.txt",
   201  					GetterMode:   structs.GetterModeAny,
   202  				},
   203  				{
   204  					GetterSource: ts.URL + "/file3.txt",
   205  					GetterMode:   structs.GetterModeAny,
   206  				},
   207  				{
   208  					GetterSource: ts.URL + "/file4.txt",
   209  					GetterMode:   structs.GetterModeAny,
   210  				},
   211  				{
   212  					GetterSource: ts.URL + "/file5.txt",
   213  					GetterMode:   structs.GetterModeAny,
   214  				},
   215  				{
   216  					GetterSource: ts.URL + "/file6.txt",
   217  					GetterMode:   structs.GetterModeAny,
   218  				},
   219  			},
   220  		},
   221  	}
   222  
   223  	resp := interfaces.TaskPrestartResponse{}
   224  
   225  	// start the hook
   226  	err := artifactHook.Prestart(context.Background(), req, &resp)
   227  
   228  	require.NoError(t, err)
   229  	require.True(t, resp.Done)
   230  	require.Len(t, resp.State, 7)
   231  	require.Len(t, me.events, 1)
   232  	require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type)
   233  
   234  	// Assert all files downloaded properly
   235  	files, err := filepath.Glob(filepath.Join(destdir, "*.txt"))
   236  	require.NoError(t, err)
   237  	require.Len(t, files, 7)
   238  	sort.Strings(files)
   239  	require.Contains(t, files[0], "file0.txt")
   240  	require.Contains(t, files[1], "file1.txt")
   241  	require.Contains(t, files[2], "file2.txt")
   242  	require.Contains(t, files[3], "file3.txt")
   243  	require.Contains(t, files[4], "file4.txt")
   244  	require.Contains(t, files[5], "file5.txt")
   245  	require.Contains(t, files[6], "file6.txt")
   246  }
   247  
   248  // TestTaskRunner_ArtifactHook_ConcurrentDownloadFailure asserts that the artifact hook
   249  // download multiple files concurrently. first iteration will result in failure and
   250  // second iteration should succeed without downloading already downloaded files.
   251  func TestTaskRunner_ArtifactHook_ConcurrentDownloadFailure(t *testing.T) {
   252  	t.Parallel()
   253  
   254  	me := &mockEmitter{}
   255  	sbox := getter.TestSandbox(t)
   256  	artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t))
   257  
   258  	// Create a source directory with 3 of the 4 artifacts
   259  	srcdir := t.TempDir()
   260  
   261  	file1 := filepath.Join(srcdir, "file1.txt")
   262  	require.NoError(t, ioutil.WriteFile(file1, []byte{'1'}, 0644))
   263  
   264  	file2 := filepath.Join(srcdir, "file2.txt")
   265  	require.NoError(t, ioutil.WriteFile(file2, []byte{'2'}, 0644))
   266  
   267  	file3 := filepath.Join(srcdir, "file3.txt")
   268  	require.NoError(t, ioutil.WriteFile(file3, []byte{'3'}, 0644))
   269  
   270  	// Test server to serve the artifacts
   271  	ts := httptest.NewServer(http.FileServer(http.Dir(srcdir)))
   272  	defer ts.Close()
   273  
   274  	// Create the target directory.
   275  	_, destdir := getter.SetupDir(t)
   276  
   277  	req := &interfaces.TaskPrestartRequest{
   278  		TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""),
   279  		TaskDir: &allocdir.TaskDir{Dir: destdir},
   280  		Task: &structs.Task{
   281  			Artifacts: []*structs.TaskArtifact{
   282  				{
   283  					GetterSource: ts.URL + "/file0.txt", // this request will fail
   284  					GetterMode:   structs.GetterModeAny,
   285  				},
   286  				{
   287  					GetterSource: ts.URL + "/file1.txt",
   288  					GetterMode:   structs.GetterModeAny,
   289  				},
   290  				{
   291  					GetterSource: ts.URL + "/file2.txt",
   292  					GetterMode:   structs.GetterModeAny,
   293  				},
   294  				{
   295  					GetterSource: ts.URL + "/file3.txt",
   296  					GetterMode:   structs.GetterModeAny,
   297  				},
   298  			},
   299  		},
   300  	}
   301  
   302  	resp := interfaces.TaskPrestartResponse{}
   303  
   304  	// On first run all files will be downloaded except file0.txt
   305  	err := artifactHook.Prestart(context.Background(), req, &resp)
   306  
   307  	require.Error(t, err)
   308  	require.True(t, structs.IsRecoverable(err))
   309  	require.Len(t, resp.State, 3)
   310  	require.False(t, resp.Done)
   311  	require.Len(t, me.events, 1)
   312  	require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type)
   313  
   314  	// delete the downloaded files so that it'll error if it's downloaded again
   315  	require.NoError(t, os.Remove(file1))
   316  	require.NoError(t, os.Remove(file2))
   317  	require.NoError(t, os.Remove(file3))
   318  
   319  	// create the missing file
   320  	file0 := filepath.Join(srcdir, "file0.txt")
   321  	require.NoError(t, ioutil.WriteFile(file0, []byte{'0'}, 0644))
   322  
   323  	// Mock TaskRunner by copying state from resp to req and reset resp.
   324  	req.PreviousState = maps.Clone(resp.State)
   325  
   326  	resp = interfaces.TaskPrestartResponse{}
   327  
   328  	// Retry the download and assert it succeeds
   329  	err = artifactHook.Prestart(context.Background(), req, &resp)
   330  	require.NoError(t, err)
   331  	require.True(t, resp.Done)
   332  	require.Len(t, resp.State, 4)
   333  
   334  	// Assert all files downloaded properly
   335  	files, err := filepath.Glob(filepath.Join(destdir, "*.txt"))
   336  	require.NoError(t, err)
   337  	sort.Strings(files)
   338  	require.Contains(t, files[0], "file0.txt")
   339  	require.Contains(t, files[1], "file1.txt")
   340  	require.Contains(t, files[2], "file2.txt")
   341  	require.Contains(t, files[3], "file3.txt")
   342  
   343  	// verify the file contents too, since files will also be created for failed downloads
   344  	data0, err := ioutil.ReadFile(files[0])
   345  	require.NoError(t, err)
   346  	require.Equal(t, data0, []byte{'0'})
   347  
   348  	data1, err := ioutil.ReadFile(files[1])
   349  	require.NoError(t, err)
   350  	require.Equal(t, data1, []byte{'1'})
   351  
   352  	data2, err := ioutil.ReadFile(files[2])
   353  	require.NoError(t, err)
   354  	require.Equal(t, data2, []byte{'2'})
   355  
   356  	data3, err := ioutil.ReadFile(files[3])
   357  	require.NoError(t, err)
   358  	require.Equal(t, data3, []byte{'3'})
   359  
   360  	require.True(t, resp.Done)
   361  	require.Len(t, resp.State, 4)
   362  }