Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 6b231d6

Browse files
authored
Merge pull request #2252 from eunomie/docker-scout-cli-hints
feat: display docker scout hints on build and pull
2 parents a0eabb3 + eb0c44c commit 6b231d6

File tree

6 files changed

+326
-4
lines changed

6 files changed

+326
-4
lines changed

api/config/config.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,6 @@ func configFilePath(dir string) string {
100100

101101
// File contains the current context from the docker configuration file
102102
type File struct {
103-
CurrentContext string `json:"currentContext,omitempty"`
103+
CurrentContext string `json:"currentContext,omitempty"`
104+
Plugins map[string]map[string]string `json:"plugins,omitempty"`
104105
}

cli/mobycli/cli_hints.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mobycli
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/docker/compose-cli/api/config"
24+
)
25+
26+
const (
27+
cliHintsEnvVarName = "DOCKER_CLI_HINTS"
28+
cliHintsDefaultBehaviour = true
29+
30+
cliHintsPluginName = "-x-cli-hints"
31+
cliHintsEnabledName = "enabled"
32+
cliHintsEnabled = "true"
33+
cliHintsDisabled = "false"
34+
)
35+
36+
func CliHintsEnabled() bool {
37+
if envValue, ok := os.LookupEnv(cliHintsEnvVarName); ok {
38+
if enabled, err := parseCliHintFlag(envValue); err == nil {
39+
return enabled
40+
}
41+
}
42+
43+
conf, err := config.LoadFile(config.Dir())
44+
if err != nil {
45+
// can't read the config file, use the default behaviour
46+
return cliHintsDefaultBehaviour
47+
}
48+
if cliHintsPluginConfig, ok := conf.Plugins[cliHintsPluginName]; ok {
49+
if cliHintsValue, ok := cliHintsPluginConfig[cliHintsEnabledName]; ok {
50+
if cliHints, err := parseCliHintFlag(cliHintsValue); err == nil {
51+
return cliHints
52+
}
53+
}
54+
}
55+
56+
return cliHintsDefaultBehaviour
57+
}
58+
59+
func parseCliHintFlag(value string) (bool, error) {
60+
switch value {
61+
case cliHintsEnabled:
62+
return true, nil
63+
case cliHintsDisabled:
64+
return false, nil
65+
default:
66+
return cliHintsDefaultBehaviour, fmt.Errorf("could not parse CLI hints enabled flag")
67+
}
68+
}

cli/mobycli/cli_hints_test.go

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mobycli
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/docker/compose-cli/api/config"
25+
26+
"gotest.tools/v3/assert"
27+
)
28+
29+
func TestCliHintsEnabled(t *testing.T) {
30+
testCases := []struct {
31+
name string
32+
setup func()
33+
expected bool
34+
}{
35+
{
36+
"enabled by default",
37+
func() {},
38+
true,
39+
},
40+
{
41+
"enabled from environment variable",
42+
func() {
43+
t.Setenv(cliHintsEnvVarName, "true")
44+
},
45+
true,
46+
},
47+
{
48+
"disabled from environment variable",
49+
func() {
50+
t.Setenv(cliHintsEnvVarName, "false")
51+
},
52+
false,
53+
},
54+
{
55+
"unsupported value",
56+
func() {
57+
t.Setenv(cliHintsEnvVarName, "maybe")
58+
},
59+
true,
60+
},
61+
{
62+
"enabled in config file",
63+
func() {
64+
d := testConfigDir(t)
65+
writeSampleConfig(t, d, configEnabled)
66+
},
67+
true,
68+
},
69+
{
70+
"plugin defined in config file but no enabled entry",
71+
func() {
72+
d := testConfigDir(t)
73+
writeSampleConfig(t, d, configPartial)
74+
},
75+
true,
76+
},
77+
78+
{
79+
"unsupported value",
80+
func() {
81+
d := testConfigDir(t)
82+
writeSampleConfig(t, d, configOnce)
83+
},
84+
true,
85+
},
86+
{
87+
"disabled in config file",
88+
func() {
89+
d := testConfigDir(t)
90+
writeSampleConfig(t, d, configDisabled)
91+
},
92+
false,
93+
},
94+
{
95+
"enabled in config file but disabled by env var",
96+
func() {
97+
d := testConfigDir(t)
98+
writeSampleConfig(t, d, configEnabled)
99+
t.Setenv(cliHintsEnvVarName, "false")
100+
},
101+
false,
102+
},
103+
{
104+
"disabled in config file but enabled by env var",
105+
func() {
106+
d := testConfigDir(t)
107+
writeSampleConfig(t, d, configDisabled)
108+
t.Setenv(cliHintsEnvVarName, "true")
109+
},
110+
true,
111+
},
112+
}
113+
114+
for _, testCase := range testCases {
115+
tc := testCase
116+
t.Run(tc.name, func(t *testing.T) {
117+
tc.setup()
118+
assert.Equal(t, CliHintsEnabled(), tc.expected)
119+
})
120+
}
121+
}
122+
123+
func testConfigDir(t *testing.T) string {
124+
dir := config.Dir()
125+
d, _ := os.MkdirTemp("", "")
126+
config.WithDir(d)
127+
t.Cleanup(func() {
128+
_ = os.RemoveAll(d)
129+
config.WithDir(dir)
130+
})
131+
return d
132+
}
133+
134+
func writeSampleConfig(t *testing.T, d string, conf []byte) {
135+
err := os.WriteFile(filepath.Join(d, config.ConfigFileName), conf, 0644)
136+
assert.NilError(t, err)
137+
}
138+
139+
var configEnabled = []byte(`{
140+
"plugins": {
141+
"-x-cli-hints": {
142+
"enabled": "true"
143+
}
144+
}
145+
}`)
146+
147+
var configDisabled = []byte(`{
148+
"plugins": {
149+
"-x-cli-hints": {
150+
"enabled": "false"
151+
}
152+
}
153+
}`)
154+
155+
var configPartial = []byte(`{
156+
"plugins": {
157+
"-x-cli-hints": {
158+
}
159+
}
160+
}`)
161+
162+
var configOnce = []byte(`{
163+
"plugins": {
164+
"-x-cli-hints": {
165+
"enabled": "maybe"
166+
}
167+
}
168+
}`)

cli/mobycli/exec.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,18 @@ func Exec(_ *cobra.Command) {
111111
}
112112
commandArgs := os.Args[1:]
113113
command := metrics.GetCommand(commandArgs)
114-
if command == "login" && !metrics.HasQuietFlag(commandArgs) {
115-
displayPATSuggestMsg(commandArgs)
114+
if !metrics.HasQuietFlag(commandArgs) {
115+
switch command {
116+
case "build": // only on regular build, not on buildx build
117+
displayScoutQuickViewSuggestMsgOnBuild(commandArgs)
118+
case "pull":
119+
displayScoutQuickViewSuggestMsgOnPull(commandArgs)
120+
case "login":
121+
displayPATSuggestMsg(commandArgs)
122+
default:
123+
}
116124
}
125+
117126
metricsClient.Track(
118127
metrics.CmdResult{
119128
ContextType: store.DefaultContextType,

cli/mobycli/scout_suggest.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package mobycli
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strings"
23+
24+
"github.com/docker/compose/v2/pkg/utils"
25+
26+
"github.com/fatih/color"
27+
)
28+
29+
func displayScoutQuickViewSuggestMsgOnPull(args []string) {
30+
image := pulledImageFromArgs(args)
31+
displayScoutQuickViewSuggestMsg(image)
32+
}
33+
34+
func displayScoutQuickViewSuggestMsgOnBuild(args []string) {
35+
// only display the hint in the main case, build command and not buildx build, no output flag, no progress flag, no push flag
36+
if utils.StringContains(args, "--output") || utils.StringContains(args, "-o") ||
37+
utils.StringContains(args, "--progress") ||
38+
utils.StringContains(args, "--push") {
39+
return
40+
}
41+
if _, ok := os.LookupEnv("BUILDKIT_PROGRESS"); ok {
42+
return
43+
}
44+
displayScoutQuickViewSuggestMsg("")
45+
}
46+
47+
func displayScoutQuickViewSuggestMsg(image string) {
48+
if !CliHintsEnabled() {
49+
return
50+
}
51+
if len(image) > 0 {
52+
image = " " + image
53+
}
54+
out := os.Stderr
55+
b := color.New(color.Bold)
56+
_, _ = fmt.Fprintln(out)
57+
_, _ = b.Fprintln(out, "What's Next?")
58+
_, _ = fmt.Fprintf(out, " View summary of image vulnerabilities and recommendations → %s", color.CyanString("docker scout quickview%s", image))
59+
_, _ = fmt.Fprintln(out)
60+
}
61+
62+
func pulledImageFromArgs(args []string) string {
63+
var image string
64+
var pull bool
65+
for _, a := range args {
66+
if a == "pull" {
67+
pull = true
68+
continue
69+
}
70+
if pull && !strings.HasPrefix(a, "-") {
71+
image = a
72+
break
73+
}
74+
}
75+
return image
76+
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/docker/docker v20.10.7+incompatible
2727
github.com/docker/go-connections v0.4.0
2828
github.com/docker/go-units v0.5.0
29+
github.com/fatih/color v1.7.0
2930
github.com/gobwas/ws v1.1.0
3031
github.com/golang/mock v1.6.0
3132
github.com/golang/protobuf v1.5.2
@@ -100,7 +101,6 @@ require (
100101
github.com/docker/go-metrics v0.0.1 // indirect
101102
github.com/evanphx/json-patch v4.9.0+incompatible // indirect
102103
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
103-
github.com/fatih/color v1.7.0 // indirect
104104
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
105105
github.com/fvbommel/sortorder v1.0.1 // indirect
106106
github.com/go-errors/errors v1.0.1 // indirect

0 commit comments

Comments
 (0)