Repository: GoogleContainerTools/skaffold (Go)

Issue:
Skaffold should error on an invalid `context` directory
### Expected behavior

Noticed by [_Rune FS_ on StackOverflow](https://stackoverflow.com/questions/62404426/skaffold-cant-find-image-it-has-just-build): Skaffold does not raise an error on startup if an artifact has an invalid context directory, and instead produces a difficult to decipher error.

### Actual behavior

```diff
--- examples/getting-started/skaffold.yaml
+++ examples/getting-started/skaffold.yaml
@@ -3,6 +3,7 @@ kind: Config
 build:
   artifacts:
   - image: skaffold-example
+    context: this/path/does/not/exist
 deploy:
   kubectl:
     manifests:
```

```console
$ skaffold build
Generating tags...
 - skaffold-example -> skaffold-example:latest
Some taggers failed. Rerun with -vdebug for errors.
Checking cache...
 - skaffold-example: Error checking cache.
getting hash for artifact "skaffold-example": getting dependencies for "skaffold-example": file pattern [main.go] must match at least one file
```

Try without the build-cache:
``` console
$ skaffold  build --cache-artifacts=false
Generating tags...
 - skaffold-example -> skaffold-example:latest
Some taggers failed. Rerun with -vdebug for errors.
Found [minikube] context, using local docker daemon.
Building [skaffold-example]...
Sending build context to Docker daemon 

couldn't build "skaffold-example": docker build: error during connect: Post "https://192.168.192.145:2376/v1.40/build?buildargs=null&cachefrom=null&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&forcerm=1&labels=null&memory=0&memswap=0&networkmode=&rm=0&shmsize=0&t=skaffold-example%3Alatest&target=&ulimits=null&version=": creating docker context: getting relative tar paths: file pattern [main.go] must match at least one file
```



Discussion:

Patch:
diff --git a/pkg/skaffold/schema/validation/validation.go b/pkg/skaffold/schema/validation/validation.go
index 41953f88837..098119c09b6 100644
--- a/pkg/skaffold/schema/validation/validation.go
+++ b/pkg/skaffold/schema/validation/validation.go
@@ -18,6 +18,7 @@ package validation
 
 import (
 	"fmt"
+	"os"
 	"reflect"
 	"strings"
 
@@ -35,6 +36,7 @@ var (
 // Process checks if the Skaffold pipeline is valid and returns all encountered errors as a concatenated string
 func Process(config *latest.SkaffoldConfig) error {
 	errs := visitStructs(config, validateYamltags)
+	errs = append(errs, validateWorkspaces(config.Build.Artifacts)...)
 	errs = append(errs, validateImageNames(config.Build.Artifacts)...)
 	errs = append(errs, validateDockerNetworkMode(config.Build.Artifacts)...)
 	errs = append(errs, validateCustomDependencies(config.Build.Artifacts)...)
@@ -54,6 +56,25 @@ func Process(config *latest.SkaffoldConfig) error {
 	return fmt.Errorf(strings.Join(messages, " | "))
 }
 
+// validateWorkspaces makes sure the artifact workspaces are valid directories.
+func validateWorkspaces(artifacts []*latest.Artifact) (errs []error) {
+	for _, a := range artifacts {
+		if a.Workspace != "" {
+			if info, err := os.Stat(a.Workspace); err != nil {
+				// err could be permission-related
+				if os.IsNotExist(err) {
+					errs = append(errs, fmt.Errorf("image %q context %q does not exist", a.ImageName, a.Workspace))
+				} else {
+					errs = append(errs, fmt.Errorf("image %q context %q: %w", a.ImageName, a.Workspace, err))
+				}
+			} else if !info.IsDir() {
+				errs = append(errs, fmt.Errorf("image %q context %q is not a directory", a.ImageName, a.Workspace))
+			}
+		}
+	}
+	return
+}
+
 // validateImageNames makes sure the artifact image names are valid base names,
 // without tags nor digests.
 func validateImageNames(artifacts []*latest.Artifact) (errs []error) {


Test patch:
diff --git a/pkg/skaffold/schema/samples_test.go b/pkg/skaffold/schema/samples_test.go
index f050718da5b..cd2b0ec7314 100644
--- a/pkg/skaffold/schema/samples_test.go
+++ b/pkg/skaffold/schema/samples_test.go
@@ -74,7 +74,15 @@ func TestParseSamples(t *testing.T) {
 }
 
 func checkSkaffoldConfig(t *testutil.T, yaml []byte) {
-	configFile := t.TempFile("skaffold.yaml", yaml)
+	root := t.NewTempDir()
+	configFile := root.Path("skaffold.yaml")
+	root.Write("skaffold.yaml", string(yaml))
+	// create workspace directories referenced in these samples
+	for _, d := range []string{"app", "node", "python", "leeroy-web", "leeroy-app", "backend", "base-service", "world-service"} {
+		root.Mkdir(d)
+	}
+	root.Chdir()
+
 	cfg, err := ParseConfigAndUpgrade(configFile, latest.Version)
 	t.CheckNoError(err)
 
diff --git a/pkg/skaffold/schema/validation/validation_test.go b/pkg/skaffold/schema/validation/validation_test.go
index 547f22d9ec6..1fe0d0c4cfc 100644
--- a/pkg/skaffold/schema/validation/validation_test.go
+++ b/pkg/skaffold/schema/validation/validation_test.go
@@ -716,3 +716,69 @@ func TestValidateJibPluginType(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateWorkspaces(t *testing.T) {
+	tmpDir := testutil.NewTempDir(t).Touch("file")
+	tmpFile := tmpDir.Path("file")
+
+	tests := []struct {
+		description string
+		artifacts   []*latest.Artifact
+		shouldErr   bool
+	}{
+		{
+			description: "no workspace",
+			artifacts: []*latest.Artifact{
+				{
+					ImageName: "image",
+				},
+			},
+		},
+		{
+			description: "directory that exists",
+			artifacts: []*latest.Artifact{
+				{
+					ImageName: "image",
+					Workspace: tmpDir.Root(),
+				},
+			},
+		},
+		{
+			description: "error on non-existent location",
+			artifacts: []*latest.Artifact{
+				{
+					ImageName: "image",
+					Workspace: "doesnotexist",
+				},
+			},
+			shouldErr: true,
+		},
+		{
+			description: "error on file",
+			artifacts: []*latest.Artifact{
+				{
+					ImageName: "image",
+					Workspace: tmpFile,
+				},
+			},
+			shouldErr: true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			// disable yamltags validation
+			t.Override(&validateYamltags, func(interface{}) error { return nil })
+
+			err := Process(
+				&latest.SkaffoldConfig{
+					Pipeline: latest.Pipeline{
+						Build: latest.BuildConfig{
+							Artifacts: test.artifacts,
+						},
+					},
+				})
+
+			t.CheckError(test.shouldErr, err)
+		})
+	}
+}