Files
nomad/client/allocrunner/taskrunner/getter/util_test.go
Chris Roberts fd1e40537c [artifact] add artifact inspection after download (#26608)
This adds artifact inspection after download to detect any issues
with the content fetched. Currently this means checking for any
symlinks within the artifact that resolve outside the task or
allocation directories. On platforms where lockdown is available
(some Linux) this inspection is not performed.

The inspection can be disabled with the DisableArtifactInspection
option. A dedicated option for disabling this behavior allows
the DisableFilesystemIsolation option to be enabled but still
have artifacts inspected after download.
2025-08-27 10:37:34 -07:00

290 lines
6.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package getter
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/go-homedir"
"github.com/shoenig/test/must"
)
func TestUtil_getURL(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
artifact *structs.TaskArtifact
expURL string
expErr *Error
}{{
name: "basic http",
artifact: &structs.TaskArtifact{GetterSource: "example.com"},
expURL: "example.com",
expErr: nil,
}, {
name: "bad url",
artifact: &structs.TaskArtifact{GetterSource: "::example.com"},
expURL: "",
expErr: &Error{
URL: "::example.com",
Err: errors.New(`failed to parse source URL "::example.com": parse "::example.com": missing protocol scheme`),
Recoverable: false,
},
}, {
name: "option",
artifact: &structs.TaskArtifact{
GetterSource: "git::github.com/hashicorp/nomad",
GetterOptions: map[string]string{"sshkey": "abc123"},
},
expURL: "git::github.com/hashicorp/nomad?sshkey=abc123",
expErr: nil,
}, {
name: "github case",
artifact: &structs.TaskArtifact{
GetterSource: "git@github.com:hashicorp/nomad.git",
GetterOptions: map[string]string{"sshkey": "abc123"},
},
expURL: "git@github.com:hashicorp/nomad.git?sshkey=abc123",
expErr: nil,
}}
env := noopTaskEnv("/path/to/task")
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result, err := getURL(env, tc.artifact)
err2, _ := err.(*Error)
must.Equal(t, tc.expErr, err2)
must.Eq(t, tc.expURL, result)
})
}
}
func TestUtil_getDestination(t *testing.T) {
ci.Parallel(t)
env := noopTaskEnv("/path/to/task")
t.Run("ok", func(t *testing.T) {
result, err := getDestination(env, &structs.TaskArtifact{
RelativeDest: "local/downloads",
})
must.NoError(t, err)
must.Eq(t, "/path/to/task/local/downloads", result)
})
t.Run("escapes", func(t *testing.T) {
result, err := getDestination(env, &structs.TaskArtifact{
RelativeDest: "../../../../../../../etc",
})
must.EqError(t, err, "artifact destination path escapes alloc directory")
must.Eq(t, "", result)
})
}
func TestUtil_getMode(t *testing.T) {
ci.Parallel(t)
cases := []struct {
mode string
exp getter.ClientMode
}{
{mode: structs.GetterModeFile, exp: getter.ClientModeFile},
{mode: structs.GetterModeDir, exp: getter.ClientModeDir},
{mode: structs.GetterModeAny, exp: getter.ClientModeAny},
}
for _, tc := range cases {
t.Run(tc.mode, func(t *testing.T) {
artifact := &structs.TaskArtifact{GetterMode: tc.mode}
result := getMode(artifact)
must.Eq(t, tc.exp, result)
})
}
}
func TestUtil_getHeaders(t *testing.T) {
ci.Parallel(t)
env := upTaskEnv("/path/to/task")
t.Run("empty", func(t *testing.T) {
result := getHeaders(env, &structs.TaskArtifact{
GetterHeaders: nil,
})
must.Nil(t, result)
})
t.Run("replacements", func(t *testing.T) {
result := getHeaders(env, &structs.TaskArtifact{
GetterHeaders: map[string]string{
"color": "red",
"number": "six",
},
})
must.MapEq(t, map[string][]string{
"Color": {"RED"},
"Number": {"SIX"},
}, result)
})
}
func TestUtil_getTaskDir(t *testing.T) {
ci.Parallel(t)
env := noopTaskEnv("/path/to/alloc/task")
allocDir, taskDir := getWritableDirs(env)
must.Eq(t, "/path/to/alloc", allocDir)
must.Eq(t, "/path/to/alloc/task", taskDir)
}
func TestUtil_environment(t *testing.T) {
// not parallel
testutil.RequireLinux(t)
homedir.DisableCache = true
t.Cleanup(func() {
homedir.DisableCache = false
})
t.Run("default", func(t *testing.T) {
t.Setenv("HOME", "/test")
result := environment("/a/b/c", "")
must.Eq(t, []string{
"HOME=/test",
"PATH=/usr/local/bin:/usr/bin:/bin",
"TMPDIR=/a/b/c/tmp",
}, result)
})
t.Run("append", func(t *testing.T) {
t.Setenv("HOME", "/test")
t.Setenv("ONE", "1")
t.Setenv("TWO", "2")
result := environment("/a/b/c", "ONE,TWO")
must.Eq(t, []string{
"HOME=/test",
"ONE=1",
"PATH=/usr/local/bin:/usr/bin:/bin",
"TMPDIR=/a/b/c/tmp",
"TWO=2",
}, result)
})
t.Run("override", func(t *testing.T) {
t.Setenv("HOME", "/test")
t.Setenv("PATH", "/opt/bin")
t.Setenv("TMPDIR", "/scratch")
result := environment("/a/b/c", "PATH,TMPDIR")
must.Eq(t, []string{
"HOME=/test",
"PATH=/opt/bin",
"TMPDIR=/scratch",
}, result)
})
t.Run("missing", func(t *testing.T) {
t.Setenv("HOME", "/test")
result := environment("/a/b/c", "DOES_NOT_EXIST")
must.Eq(t, []string{
"DOES_NOT_EXIST=",
"HOME=/test",
"PATH=/usr/local/bin:/usr/bin:/bin",
"TMPDIR=/a/b/c/tmp",
}, result)
})
t.Run("homeless non-root", func(t *testing.T) {
testutil.RequireNonRoot(t)
// assert we fallback via go-homdir ...
userHome, err := homedir.Dir()
must.NoError(t, err)
// ... when HOME env var is not set, as is the case in some systemd setups
t.Setenv("HOME", "")
result := environment("/a/b/c", "")
must.Eq(t, []string{
fmt.Sprintf("HOME=%s", userHome),
"PATH=/usr/local/bin:/usr/bin:/bin",
"TMPDIR=/a/b/c/tmp",
}, result)
})
t.Run("homeless root", func(t *testing.T) {
testutil.RequireRoot(t)
t.Setenv("HOME", "/root") // fake running as full root
// assert we fallback via go-homdir ...
userHome, err := homedir.Dir()
must.NoError(t, err)
// ... when HOME env var is not set, as is the case in some systemd setups
t.Setenv("HOME", "")
result := environment("/a/b/c", "")
must.Eq(t, []string{
fmt.Sprintf("HOME=%s", userHome),
"PATH=/usr/local/bin:/usr/bin:/bin",
"TMPDIR=/a/b/c/tmp",
}, result)
})
}
func TestUtil_isPathWithin(t *testing.T) {
tdir := t.TempDir()
pathFn := func(parent string) string {
dir, err := os.MkdirTemp(parent, "testing-path")
must.NoError(t, err, must.Sprint("failed to create temporary directory"))
return dir
}
t.Run("when path not within root", func(t *testing.T) {
root := pathFn(tdir)
check := pathFn(tdir)
result, err := isPathWithin(root, check)
must.NoError(t, err)
must.False(t, result)
})
t.Run("when path within root", func(t *testing.T) {
root := pathFn(tdir)
check := pathFn(root)
result, err := isPathWithin(root, check)
must.NoError(t, err)
must.True(t, result)
})
t.Run("when root within path", func(t *testing.T) {
check := pathFn(tdir)
root := pathFn(check)
result, err := isPathWithin(root, check)
must.NoError(t, err)
must.False(t, result)
})
t.Run("when path does not exist", func(t *testing.T) {
root := filepath.Join(tdir, "missing")
check := filepath.Join(root, "unknown")
result, err := isPathWithin(root, check)
must.ErrorContains(t, err, "no such file or directory")
must.False(t, result)
})
}