diff --git a/internal/util/cmdutil/cmdutil.go b/internal/util/cmdutil/cmdutil.go index fa5ffd752e..3efc7d8294 100644 --- a/internal/util/cmdutil/cmdutil.go +++ b/internal/util/cmdutil/cmdutil.go @@ -35,12 +35,13 @@ import ( ) const ( - StackTraceOnErrors = "COBRA_STACK_TRACE_ON_ERRORS" - trueString = "true" - Stdout = "stdout" - Unwrap = "unwrap" - dockerVersionTimeout time.Duration = 5 * time.Second - FunctionsCatalogURL = "https://catalog.kpt.dev/catalog-v2.json" + StackTraceOnErrors = "COBRA_STACK_TRACE_ON_ERRORS" + trueString = "true" + Stdout = "stdout" + Unwrap = "unwrap" + dockerVersionTimeout time.Duration = 5 * time.Second + FunctionsCatalogURL = "https://catalog.kpt.dev/catalog-v2.json" + minSupportedDockerVersion string = "v20.10.0" ) // FixDocs replaces instances of old with new in the docs for c @@ -88,22 +89,39 @@ func ResolveAbsAndRelPaths(path string) (string, string, error) { return relPath, absPath, nil } -// DockerCmdAvailable runs `docker ps` to check that the docker command is -// available, and returns an error with installation instructions if it is not +// DockerCmdAvailable runs `docker version` to check that the docker command is +// available and is a supported version. Returns an error with installation +// instructions if it is not func DockerCmdAvailable() error { suggestedText := `docker must be running to use this command To install docker, follow the instructions at https://docs.docker.com/get-docker/. ` - buffer := &bytes.Buffer{} + cmdOut := &bytes.Buffer{} ctx, cancel := context.WithTimeout(context.Background(), dockerVersionTimeout) defer cancel() - cmd := exec.CommandContext(ctx, "docker", "version") - cmd.Stderr = buffer + cmd := exec.CommandContext(ctx, "docker", "version", "--format", "{{.Client.Version}}") + cmd.Stdout = cmdOut err := cmd.Run() - if err != nil { + if err != nil || cmdOut.String() == "" { return fmt.Errorf("%s", suggestedText) } + return isSupportedDockerVersion(strings.TrimSuffix(cmdOut.String(), "\n")) +} + +// isSupportedDockerVersion returns an error if a given docker version is invalid +// or is less than minSupportedDockerVersion +func isSupportedDockerVersion(v string) error { + suggestedText := fmt.Sprintf(`docker client version must be %s or greater`, minSupportedDockerVersion) + // docker version output does not have a leading v which is required by semver, so we prefix it + currentDockerVersion := fmt.Sprintf("v%s", v) + if !semver.IsValid(currentDockerVersion) { + return fmt.Errorf("%s: found invalid version %s", suggestedText, currentDockerVersion) + } + // if currentDockerVersion is less than minDockerClientVersion, compare returns +1 + if semver.Compare(minSupportedDockerVersion, currentDockerVersion) > 0 { + return fmt.Errorf("%s: found %s", suggestedText, currentDockerVersion) + } return nil } diff --git a/internal/util/cmdutil/cmdutil_test.go b/internal/util/cmdutil/cmdutil_test.go index c73792b56c..e3adfc1742 100644 --- a/internal/util/cmdutil/cmdutil_test.go +++ b/internal/util/cmdutil/cmdutil_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/kyaml/kio" ) @@ -352,3 +353,43 @@ func TestListImages(t *testing.T) { sort.Strings(result) assert.Equal(t, []string{"apply-setters:v0.1.1", "gatekeeper:v0.2.1"}, result) } + +func TestIsSupportedDockerVersion(t *testing.T) { + tests := []struct { + name string + inputV string + errMsg string + }{ + { + name: "greater than min version", + inputV: "20.10.1", + }, + { + name: "equal to min version", + inputV: "20.10.0", + }, + { + name: "less than min version", + inputV: "20.9.1", + errMsg: "docker client version must be v20.10.0 or greater: found v20.9.1", + }, + { + name: "invalid semver", + inputV: "20..12.1", + errMsg: "docker client version must be v20.10.0 or greater: found invalid version v20..12.1", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + err := isSupportedDockerVersion(tt.inputV) + if tt.errMsg != "" { + require.NotNil(err) + require.Contains(err.Error(), tt.errMsg) + } else { + require.NoError(err) + } + }) + } +}