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

Commit

Permalink
Merge pull request #2252 from eunomie/docker-scout-cli-hints
Browse files Browse the repository at this point in the history
feat: display docker scout hints on build and pull
  • Loading branch information
glours committed Jun 21, 2023
2 parents a0eabb3 + eb0c44c commit 6b231d6
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 4 deletions.
3 changes: 2 additions & 1 deletion api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,6 @@ func configFilePath(dir string) string {

// File contains the current context from the docker configuration file
type File struct {
CurrentContext string `json:"currentContext,omitempty"`
CurrentContext string `json:"currentContext,omitempty"`
Plugins map[string]map[string]string `json:"plugins,omitempty"`
}
68 changes: 68 additions & 0 deletions cli/mobycli/cli_hints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2023 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 mobycli

import (
"fmt"
"os"

"github.com/docker/compose-cli/api/config"
)

const (
cliHintsEnvVarName = "DOCKER_CLI_HINTS"
cliHintsDefaultBehaviour = true

cliHintsPluginName = "-x-cli-hints"
cliHintsEnabledName = "enabled"
cliHintsEnabled = "true"
cliHintsDisabled = "false"
)

func CliHintsEnabled() bool {
if envValue, ok := os.LookupEnv(cliHintsEnvVarName); ok {
if enabled, err := parseCliHintFlag(envValue); err == nil {
return enabled
}
}

conf, err := config.LoadFile(config.Dir())
if err != nil {
// can't read the config file, use the default behaviour
return cliHintsDefaultBehaviour
}
if cliHintsPluginConfig, ok := conf.Plugins[cliHintsPluginName]; ok {
if cliHintsValue, ok := cliHintsPluginConfig[cliHintsEnabledName]; ok {
if cliHints, err := parseCliHintFlag(cliHintsValue); err == nil {
return cliHints
}
}
}

return cliHintsDefaultBehaviour
}

func parseCliHintFlag(value string) (bool, error) {
switch value {
case cliHintsEnabled:
return true, nil
case cliHintsDisabled:
return false, nil
default:
return cliHintsDefaultBehaviour, fmt.Errorf("could not parse CLI hints enabled flag")
}
}
168 changes: 168 additions & 0 deletions cli/mobycli/cli_hints_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
Copyright 2023 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 mobycli

import (
"os"
"path/filepath"
"testing"

"github.com/docker/compose-cli/api/config"

"gotest.tools/v3/assert"
)

func TestCliHintsEnabled(t *testing.T) {
testCases := []struct {
name string
setup func()
expected bool
}{
{
"enabled by default",
func() {},
true,
},
{
"enabled from environment variable",
func() {
t.Setenv(cliHintsEnvVarName, "true")
},
true,
},
{
"disabled from environment variable",
func() {
t.Setenv(cliHintsEnvVarName, "false")
},
false,
},
{
"unsupported value",
func() {
t.Setenv(cliHintsEnvVarName, "maybe")
},
true,
},
{
"enabled in config file",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configEnabled)
},
true,
},
{
"plugin defined in config file but no enabled entry",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configPartial)
},
true,
},

{
"unsupported value",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configOnce)
},
true,
},
{
"disabled in config file",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configDisabled)
},
false,
},
{
"enabled in config file but disabled by env var",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configEnabled)
t.Setenv(cliHintsEnvVarName, "false")
},
false,
},
{
"disabled in config file but enabled by env var",
func() {
d := testConfigDir(t)
writeSampleConfig(t, d, configDisabled)
t.Setenv(cliHintsEnvVarName, "true")
},
true,
},
}

for _, testCase := range testCases {
tc := testCase
t.Run(tc.name, func(t *testing.T) {
tc.setup()
assert.Equal(t, CliHintsEnabled(), tc.expected)
})
}
}

func testConfigDir(t *testing.T) string {
dir := config.Dir()
d, _ := os.MkdirTemp("", "")
config.WithDir(d)
t.Cleanup(func() {
_ = os.RemoveAll(d)
config.WithDir(dir)
})
return d
}

func writeSampleConfig(t *testing.T, d string, conf []byte) {
err := os.WriteFile(filepath.Join(d, config.ConfigFileName), conf, 0644)
assert.NilError(t, err)
}

var configEnabled = []byte(`{
"plugins": {
"-x-cli-hints": {
"enabled": "true"
}
}
}`)

var configDisabled = []byte(`{
"plugins": {
"-x-cli-hints": {
"enabled": "false"
}
}
}`)

var configPartial = []byte(`{
"plugins": {
"-x-cli-hints": {
}
}
}`)

var configOnce = []byte(`{
"plugins": {
"-x-cli-hints": {
"enabled": "maybe"
}
}
}`)
13 changes: 11 additions & 2 deletions cli/mobycli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,18 @@ func Exec(_ *cobra.Command) {
}
commandArgs := os.Args[1:]
command := metrics.GetCommand(commandArgs)
if command == "login" && !metrics.HasQuietFlag(commandArgs) {
displayPATSuggestMsg(commandArgs)
if !metrics.HasQuietFlag(commandArgs) {
switch command {
case "build": // only on regular build, not on buildx build
displayScoutQuickViewSuggestMsgOnBuild(commandArgs)
case "pull":
displayScoutQuickViewSuggestMsgOnPull(commandArgs)
case "login":
displayPATSuggestMsg(commandArgs)
default:
}
}

metricsClient.Track(
metrics.CmdResult{
ContextType: store.DefaultContextType,
Expand Down
76 changes: 76 additions & 0 deletions cli/mobycli/scout_suggest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright 2023 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 mobycli

import (
"fmt"
"os"
"strings"

"github.com/docker/compose/v2/pkg/utils"

"github.com/fatih/color"
)

func displayScoutQuickViewSuggestMsgOnPull(args []string) {
image := pulledImageFromArgs(args)
displayScoutQuickViewSuggestMsg(image)
}

func displayScoutQuickViewSuggestMsgOnBuild(args []string) {
// only display the hint in the main case, build command and not buildx build, no output flag, no progress flag, no push flag
if utils.StringContains(args, "--output") || utils.StringContains(args, "-o") ||
utils.StringContains(args, "--progress") ||
utils.StringContains(args, "--push") {
return
}
if _, ok := os.LookupEnv("BUILDKIT_PROGRESS"); ok {
return
}
displayScoutQuickViewSuggestMsg("")
}

func displayScoutQuickViewSuggestMsg(image string) {
if !CliHintsEnabled() {
return
}
if len(image) > 0 {
image = " " + image
}
out := os.Stderr
b := color.New(color.Bold)
_, _ = fmt.Fprintln(out)
_, _ = b.Fprintln(out, "What's Next?")
_, _ = fmt.Fprintf(out, " View summary of image vulnerabilities and recommendations → %s", color.CyanString("docker scout quickview%s", image))
_, _ = fmt.Fprintln(out)
}

func pulledImageFromArgs(args []string) string {
var image string
var pull bool
for _, a := range args {
if a == "pull" {
pull = true
continue
}
if pull && !strings.HasPrefix(a, "-") {
image = a
break
}
}
return image
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
github.com/fatih/color v1.7.0
github.com/gobwas/ws v1.1.0
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2
Expand Down Expand Up @@ -100,7 +101,6 @@ require (
github.com/docker/go-metrics v0.0.1 // indirect
github.com/evanphx/json-patch v4.9.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/go-errors/errors v1.0.1 // indirect
Expand Down

0 comments on commit 6b231d6

Please sign in to comment.