Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add config command #44

Merged
merged 6 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
# TODO: enable this when we have coverage on docstring comments
#issues:
issues:
max-same-issues: 25

# TODO: enable this when we have coverage on docstring comments
# # The list of ids of default excludes to include or disable.
# include:
# - EXC0002 # disable excluding of issues about comments from golint

linters-settings:
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 80
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 60

linters:
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
- asciicheck
- bodyclose
- depguard
- dogsled
- dupl
- errcheck
Expand All @@ -40,7 +30,6 @@ linters:
- ineffassign
- misspell
- nakedret
- nolintlint
- revive
- staticcheck
- stylecheck
Expand All @@ -50,23 +39,44 @@ linters:
- unused
- whitespace

linters-settings:
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 80
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: 60
output:
uniq-by-line: false
run:
timeout: 10m

# do not enable...
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
# - depguard # We don't have a configuration for this yet
# - goprintffuncname # does not catch all cases and there are exceptions
# - nakedret # does not catch all cases and should not fail a build
# - gochecknoglobals
# - gochecknoinits # this is too aggressive
# - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649
# - godot
# - godox
# - goerr113
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818)
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - nestif
# - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint)
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - rowserrcheck # not in a repo with sql, so this is not useful
# - scopelint # deprecated
# - structcheck # The owner seems to have abandoned the linter. Replaced by "unused".
# - testpackage
# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused".
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
# - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
# - rowserrcheck # we're not using sql.Rows at all in the codebase
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ LINT_CMD = $(TEMP_DIR)/golangci-lint run --tests=false --timeout=2m --config .go
GOIMPORTS_CMD = $(TEMP_DIR)/gosimports -local github.com/anchore

# Tool versions #################################
GOLANG_CI_VERSION = v1.52.2
GOLANG_CI_VERSION = v1.55.1
GOBOUNCER_VERSION = v0.4.0
GOSIMPORTS_VERSION = v0.3.8

Expand Down
182 changes: 182 additions & 0 deletions config_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package clio

import (
"errors"
"fmt"
"os"
"reflect"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"

"github.com/anchore/fangs"
)

func ConfigCommand(app Application, options ...configCommandOption) *cobra.Command {
opts := configCommandOptions{}
for _, option := range options {
option(&opts)
}

id := app.ID()
internalApp := extractInternalApp(app)
if internalApp == nil {
return &cobra.Command{
RunE: func(_ *cobra.Command, _ []string) error {
return fmt.Errorf("unable to extract internal application, provided: %v", app)
},
}
}

cmd := &cobra.Command{
Use: "config",
Short: fmt.Sprintf("show the %s configuration", id.Name),
RunE: func(cmd *cobra.Command, _ []string) error {
allConfigs := allCommandConfigs(internalApp)
var err error
if opts.loadConfig {
err = loadAllConfigs(cmd, internalApp.setupConfig.FangsConfig, allConfigs)
}
filter := opts.valueFilterFunc
if internalApp.state.RedactStore != nil {
filter = chainFilterFuncs(internalApp.state.RedactStore.RedactString, filter)
}
summary := summarizeConfig(cmd, internalApp.setupConfig.FangsConfig, filter, allConfigs)
_, writeErr := os.Stdout.WriteString(summary)
if writeErr != nil {
writeErr = fmt.Errorf("an error occurred writing configuration summary: %w", writeErr)
err = errors.Join(err, writeErr)
}
if err != nil {
// space before the error display
_, _ = os.Stderr.WriteString("\n")
}
return err
},
}

cmd.Flags().BoolVarP(&opts.loadConfig, "load", "", opts.loadConfig, fmt.Sprintf("load and validate the %s configuration", id.Name))

if opts.includeLocationsSubcommand {
// sub-command to print expanded configuration file search locations
cmd.AddCommand(summarizeLocationsCommand(internalApp))
}

return cmd
}

type configCommandOption func(*configCommandOptions)

type valueFilterFunc func(string) string

type configCommandOptions struct {
loadConfig bool
includeLocationsSubcommand bool
valueFilterFunc valueFilterFunc
}

// ReplaceHomeDirWithTilde adds a value filter function which replaces matching home directory values in strings
// starting with the user's home directory to make configurations more portable
func ReplaceHomeDirWithTilde(opts *configCommandOptions) {
wagoodman marked this conversation as resolved.
Show resolved Hide resolved
userHome, _ := homedir.Dir()
if userHome != "" {
opts.valueFilterFunc = chainFilterFuncs(opts.valueFilterFunc, func(s string) string {
// make any defaults based on the user's home directory more portable
if strings.HasPrefix(s, userHome) {
s = strings.ReplaceAll(s, userHome, "~")
}
return s
})
}
}

// IncludeLocationsSubcommand will include a `config locations` subcommand which lists each location that will be used
// to locate configuration files based on the configured environment
func IncludeLocationsSubcommand(opts *configCommandOptions) {
opts.includeLocationsSubcommand = true
}

func chainFilterFuncs(f1, f2 valueFilterFunc) valueFilterFunc {
if f1 == nil {
return f2
}
if f2 == nil {
return f1
}
return func(s string) string {
s = f1(s)
s = f2(s)
return s
}
}

func extractInternalApp(app Application) *application {
if a, ok := app.(*application); ok {
return a
}
return nil
}

func allCommandConfigs(internalApp *application) []any {
return append([]any{&internalApp.state.Config, internalApp}, internalApp.state.Config.FromCommands...)
}

func loadAllConfigs(cmd *cobra.Command, fangsCfg fangs.Config, allConfigs []any) error {
var errs []error
for _, cfg := range allConfigs {
// load each config individually, as there may be conflicting names / types that will cause
// viper to fail to read them all and panic
if err := fangs.Load(fangsCfg, cmd, cfg); err != nil {
t := reflect.TypeOf(cfg)
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
errs = append(errs, fmt.Errorf("error loading config %s: %w", t.Name(), err))
}
}
if len(errs) == 0 {
return nil
}
return fmt.Errorf("error(s) occurred loading configuration: %w", errors.Join(errs...))
}

func summarizeConfig(commandWithRootParent *cobra.Command, fangsCfg fangs.Config, redact func(string) string, allConfigs []any) string {
summary := fangs.SummarizeCommand(fangsCfg, commandWithRootParent, redact, allConfigs...)
summary = strings.TrimSpace(summary) + "\n"
return summary
}

func summarizeLocationsCommand(internalApp *application) *cobra.Command {
var all bool

cmd := &cobra.Command{
Use: "locations",
Short: fmt.Sprintf("shows all locations and the order in which %s will look for a configuration file", internalApp.ID().Name),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
suffix := ".yaml"
if all {
suffix = ""
}
summary := summarizeLocations(internalApp.setupConfig.FangsConfig, suffix)
_, err := os.Stdout.WriteString(summary)
return err
},
}

cmd.Flags().BoolVarP(&all, "all", "", all, "include every file extension supported")

return cmd
}

func summarizeLocations(fangsCfg fangs.Config, onlySuffix string) string {
out := ""
for _, f := range fangs.SummarizeLocations(fangsCfg) {
if onlySuffix != "" && !strings.HasSuffix(f, onlySuffix) {
continue
}
out += f + "\n"
}
return out
}
Loading
Loading