mirror of https://github.com/docker/compose.git
Categorize failure metrics, set specific metrics for compose file parse error, file not found, cmdline syntax errors, build error, pull error.
Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>
This commit is contained in:
parent
c11a458666
commit
073d8e5545
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
"github.com/docker/compose-cli/api/context/store"
|
||||
"github.com/docker/compose-cli/cli/formatter"
|
||||
"github.com/docker/compose-cli/cli/metrics"
|
||||
)
|
||||
|
||||
// Warning is a global warning to be displayed to user on command failure
|
||||
|
@ -74,7 +75,7 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
|
|||
|
||||
project, err := cli.ProjectFromOptions(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, metrics.WrapComposeError(err)
|
||||
}
|
||||
|
||||
if len(services) != 0 {
|
||||
|
@ -114,6 +115,14 @@ func Command(contextType string) *cobra.Command {
|
|||
Short: "Docker Compose",
|
||||
Use: "compose",
|
||||
TraverseChildren: true,
|
||||
// By default (no Run/RunE in parent command) for typos in subcommands, cobra displays the help of parent command but exit(0) !
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
_ = cmd.Help()
|
||||
return fmt.Errorf("unknown docker command: %q", "compose "+args[0])
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if noAnsi {
|
||||
if ansi != "auto" {
|
||||
|
|
20
cli/main.go
20
cli/main.go
|
@ -70,7 +70,7 @@ var (
|
|||
"version": {},
|
||||
"backend-metadata": {},
|
||||
}
|
||||
unknownCommandRegexp = regexp.MustCompile(`unknown command "([^"]*)"`)
|
||||
unknownCommandRegexp = regexp.MustCompile(`unknown docker command: "([^"]*)"`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -122,7 +122,7 @@ func main() {
|
|||
if len(args) == 0 {
|
||||
return cmd.Help()
|
||||
}
|
||||
return fmt.Errorf("unknown command %q", args[0])
|
||||
return fmt.Errorf("unknown docker command: %q", args[0])
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -292,7 +292,18 @@ func exit(ctx string, err error, ctype string) {
|
|||
os.Exit(exit.StatusCode)
|
||||
}
|
||||
|
||||
metrics.Track(ctype, os.Args[1:], metrics.FailureStatus)
|
||||
var composeErr metrics.ComposeError
|
||||
metricsStatus := metrics.FailureStatus
|
||||
exitCode := 1
|
||||
if errors.As(err, &composeErr) {
|
||||
metricsStatus = composeErr.GetMetricsFailureCategory().MetricsStatus
|
||||
exitCode = composeErr.GetMetricsFailureCategory().ExitCode
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "unknown shorthand flag:") || strings.HasPrefix(err.Error(), "unknown flag:") || strings.HasPrefix(err.Error(), "unknown docker command:") {
|
||||
metricsStatus = metrics.CommandSyntaxFailure.MetricsStatus
|
||||
exitCode = metrics.CommandSyntaxFailure.ExitCode
|
||||
}
|
||||
metrics.Track(ctype, os.Args[1:], metricsStatus)
|
||||
|
||||
if errors.Is(err, errdefs.ErrLoginRequired) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
@ -310,7 +321,8 @@ func exit(ctx string, err error, ctype string) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
fatal(err)
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
|
|
|
@ -47,17 +47,6 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// APISource is sent for API metrics
|
||||
APISource = "api"
|
||||
// SuccessStatus is sent for API metrics
|
||||
SuccessStatus = "success"
|
||||
// FailureStatus is sent for API metrics
|
||||
FailureStatus = "failure"
|
||||
// CanceledStatus is sent for API metrics
|
||||
CanceledStatus = "canceled"
|
||||
)
|
||||
|
||||
// Client sends metrics to Docker Desktopn
|
||||
type Client interface {
|
||||
// Send sends the command to Docker Desktop. Note that the function doesn't
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
composeerrdefs "github.com/compose-spec/compose-go/errdefs"
|
||||
)
|
||||
|
||||
// ComposeError error to categorize failures and extract metrics info
|
||||
type ComposeError struct {
|
||||
Err error
|
||||
Category *FailureCategory
|
||||
}
|
||||
|
||||
// WrapComposeError wraps the error if not nil, otherwise returns nil
|
||||
func WrapComposeError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return ComposeError{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// WrapCategorisedComposeError wraps the error if not nil, otherwise returns nil
|
||||
func WrapCategorisedComposeError(err error, failure FailureCategory) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return ComposeError{
|
||||
Err: err,
|
||||
Category: &failure,
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap get underlying error
|
||||
func (e ComposeError) Unwrap() error { return e.Err }
|
||||
|
||||
func (e ComposeError) Error() string { return e.Err.Error() }
|
||||
|
||||
// GetMetricsFailureCategory get metrics status and error code corresponding to this error
|
||||
func (e ComposeError) GetMetricsFailureCategory() FailureCategory {
|
||||
if e.Category != nil {
|
||||
return *e.Category
|
||||
}
|
||||
var pathError *fs.PathError
|
||||
if errors.As(e.Err, &pathError) {
|
||||
return FileNotFoundFailure
|
||||
}
|
||||
if composeerrdefs.IsNotFoundError(e.Err) {
|
||||
return FileNotFoundFailure
|
||||
}
|
||||
return ComposeParseFailure
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
// FailureCategory sruct regrouping metrics failure status and specific exit code
|
||||
type FailureCategory struct {
|
||||
MetricsStatus string
|
||||
ExitCode int
|
||||
}
|
||||
|
||||
const (
|
||||
// APISource is sent for API metrics
|
||||
APISource = "api"
|
||||
// SuccessStatus command success
|
||||
SuccessStatus = "success"
|
||||
// FailureStatus command failure
|
||||
FailureStatus = "failure"
|
||||
// ComposeParseFailureStatus failure while parsing compose file
|
||||
ComposeParseFailureStatus = "failure-compose-parse"
|
||||
// FileNotFoundFailureStatus failure getting compose file
|
||||
FileNotFoundFailureStatus = "failure-file-not-found"
|
||||
// CommandSyntaxFailureStatus failure reading command
|
||||
CommandSyntaxFailureStatus = "failure-cmd-syntax"
|
||||
// BuildFailureStatus failure building imge
|
||||
BuildFailureStatus = "failure-build"
|
||||
// PullFailureStatus failure pulling imge
|
||||
PullFailureStatus = "failure-pull"
|
||||
// CanceledStatus command canceled
|
||||
CanceledStatus = "canceled"
|
||||
)
|
||||
|
||||
var (
|
||||
// FileNotFoundFailure failure for compose file not found
|
||||
FileNotFoundFailure = FailureCategory{MetricsStatus: FileNotFoundFailureStatus, ExitCode: 14}
|
||||
// ComposeParseFailure failure for composefile parse error
|
||||
ComposeParseFailure = FailureCategory{MetricsStatus: ComposeParseFailureStatus, ExitCode: 15}
|
||||
// CommandSyntaxFailure failure for command line syntax
|
||||
CommandSyntaxFailure = FailureCategory{MetricsStatus: CommandSyntaxFailureStatus, ExitCode: 16}
|
||||
//BuildFailure failure while building images.
|
||||
BuildFailure = FailureCategory{MetricsStatus: BuildFailureStatus, ExitCode: 17}
|
||||
// PullFailure failure while pulling image
|
||||
PullFailure = FailureCategory{MetricsStatus: PullFailureStatus, ExitCode: 18}
|
||||
)
|
|
@ -24,21 +24,21 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
moby "github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
composeprogress "github.com/docker/compose-cli/api/progress"
|
||||
"github.com/docker/compose-cli/utils"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/driver"
|
||||
_ "github.com/docker/buildx/driver/docker" // required to get default driver registered
|
||||
"github.com/docker/buildx/util/progress"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
bclient "github.com/moby/buildkit/client"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/docker/compose-cli/api/compose"
|
||||
composeprogress "github.com/docker/compose-cli/api/progress"
|
||||
"github.com/docker/compose-cli/cli/metrics"
|
||||
"github.com/docker/compose-cli/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Build(ctx context.Context, project *types.Project, options compose.BuildOptions) error {
|
||||
|
@ -160,7 +160,8 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opts
|
|||
if info.OSType == "windows" {
|
||||
// no support yet for Windows container builds in Buildkit
|
||||
// https://docs.docker.com/develop/develop-images/build_enhancements/#limitations
|
||||
return s.windowsBuild(opts, mode)
|
||||
err := s.windowsBuild(opts, mode)
|
||||
return metrics.WrapCategorisedComposeError(err, metrics.BuildFailure)
|
||||
}
|
||||
if len(opts) == 0 {
|
||||
return nil
|
||||
|
@ -190,6 +191,9 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opts
|
|||
if err == nil {
|
||||
err = errW
|
||||
}
|
||||
if err != nil {
|
||||
return metrics.WrapCategorisedComposeError(err, metrics.BuildFailure)
|
||||
}
|
||||
|
||||
cw := composeprogress.ContextWriter(ctx)
|
||||
for _, c := range observedState {
|
||||
|
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/docker/compose-cli/api/compose"
|
||||
"github.com/docker/compose-cli/api/config"
|
||||
"github.com/docker/compose-cli/api/progress"
|
||||
"github.com/docker/compose-cli/cli/metrics"
|
||||
)
|
||||
|
||||
func (s *composeService) Pull(ctx context.Context, project *types.Project, opts compose.PullOptions) error {
|
||||
|
@ -121,7 +122,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
|||
Status: progress.Error,
|
||||
Text: "Error",
|
||||
})
|
||||
return err
|
||||
return metrics.WrapCategorisedComposeError(err, metrics.PullFailure)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(stream)
|
||||
|
@ -131,10 +132,10 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
|||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
return metrics.WrapCategorisedComposeError(err, metrics.PullFailure)
|
||||
}
|
||||
if jm.Error != nil {
|
||||
return errors.New(jm.Error.Message)
|
||||
return metrics.WrapCategorisedComposeError(errors.New(jm.Error.Message), metrics.PullFailure)
|
||||
}
|
||||
toPullProgressEvent(service.Name, jm, w)
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ func TestContextMetrics(t *testing.T) {
|
|||
assert.DeepEqual(t, []string{
|
||||
`{"command":"ps","context":"moby","source":"cli","status":"success"}`,
|
||||
`{"command":"version","context":"moby","source":"cli","status":"success"}`,
|
||||
`{"command":"version","context":"moby","source":"cli","status":"failure"}`,
|
||||
`{"command":"version","context":"moby","source":"cli","status":"failure-cmd-syntax"}`,
|
||||
}, usage)
|
||||
})
|
||||
|
||||
|
@ -224,7 +224,6 @@ func TestContextDuplicateACI(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestContextRemove(t *testing.T) {
|
||||
|
||||
t.Run("remove current", func(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
|
||||
|
|
|
@ -95,9 +95,9 @@ func TestLocalComposeRun(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("compose run --publish", func(t *testing.T) {
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yml", "run", "--publish", "8080:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
|
||||
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yml", "run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
|
||||
res := c.RunDockerCmd("ps")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "8080->80/tcp"), res.Stdout())
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
services:
|
||||
simple:
|
||||
build: service1
|
|
@ -0,0 +1,4 @@
|
|||
services:
|
||||
simple:
|
||||
image: nginx
|
||||
wrongField: test
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM nginx
|
||||
|
||||
WRONG DOCKERFILE
|
|
@ -0,0 +1,3 @@
|
|||
services:
|
||||
simple:
|
||||
image: unknownimage
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
|
||||
. "github.com/docker/compose-cli/utils/e2e"
|
||||
)
|
||||
|
||||
func TestComposeMetrics(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
s := NewMetricsServer(c.MetricsSocket())
|
||||
s.Start()
|
||||
defer s.Stop()
|
||||
|
||||
started := false
|
||||
for i := 0; i < 30; i++ {
|
||||
c.RunDockerCmd("help", "ps")
|
||||
if len(s.GetUsage()) > 0 {
|
||||
started = true
|
||||
fmt.Printf(" [%s] Server up in %d ms\n", t.Name(), i*100)
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
assert.Assert(t, started, "Metrics mock server not available after 3 secs")
|
||||
|
||||
t.Run("catch specific failure metrics", func(t *testing.T) {
|
||||
s.ResetUsage()
|
||||
|
||||
res := c.RunDockerOrExitError("compose", "-f", "../compose/fixtures/does-not-exist/compose.yml", "build")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 14, Err: "compose/fixtures/does-not-exist/compose.yml: no such file or directory"})
|
||||
res = c.RunDockerOrExitError("compose", "-f", "../compose/fixtures/wrong-composefile/compose.yml", "up", "-d")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 15, Err: "services.simple Additional property wrongField is not allowed"})
|
||||
res = c.RunDockerOrExitError("compose", "up")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 14, Err: "can't find a suitable configuration file in this directory or any parent: not found"})
|
||||
res = c.RunDockerOrExitError("compose", "up", "-f", "../compose/fixtures/wrong-composefile/compose.yml")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 16, Err: "unknown shorthand flag: 'f' in -f"})
|
||||
res = c.RunDockerOrExitError("compose", "up", "--file", "../compose/fixtures/wrong-composefile/compose.yml")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 16, Err: "unknown flag: --file"})
|
||||
res = c.RunDockerOrExitError("compose", "donw", "--file", "../compose/fixtures/wrong-composefile/compose.yml")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 16, Err: `unknown docker command: "compose donw"`})
|
||||
res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/unknown-image.yml", "pull")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 18, Err: `pull access denied for unknownimage, repository does not exist or may require 'docker login'`})
|
||||
res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/build-error.yml", "build")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 17, Err: `line 17: unknown instruction: WRONG`})
|
||||
res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/build-error.yml", "up")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 17, Err: `line 17: unknown instruction: WRONG`})
|
||||
res = c.RunDockerOrExitError("compose", "--file", "../compose/fixtures/wrong-composefile/unknown-image.yml", "up")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 17, Err: `pull access denied, repository does not exist or may require authorization`})
|
||||
|
||||
usage := s.GetUsage()
|
||||
assert.DeepEqual(t, []string{
|
||||
`{"command":"compose build","context":"moby","source":"cli","status":"failure-file-not-found"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-compose-parse"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-file-not-found"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-cmd-syntax"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-cmd-syntax"}`,
|
||||
`{"command":"compose","context":"moby","source":"cli","status":"failure-cmd-syntax"}`,
|
||||
`{"command":"compose pull","context":"moby","source":"cli","status":"failure-pull"}`,
|
||||
`{"command":"compose build","context":"moby","source":"cli","status":"failure-build"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-build"}`,
|
||||
`{"command":"compose up","context":"moby","source":"cli","status":"failure-build"}`,
|
||||
}, usage)
|
||||
})
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
Loading…
Reference in New Issue