Skip to content

Commit

Permalink
feat: add config command (#44)
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <[email protected]>
  • Loading branch information
kzantow authored May 22, 2024
1 parent d03a618 commit d81e109
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 515 deletions.
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
196 changes: 196 additions & 0 deletions config_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package clio

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

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

"github.com/anchore/fangs"
"github.com/anchore/go-logger/adapter/redact"
)

func ConfigCommand(app Application, opts *ConfigCommandConfig) *cobra.Command {
if opts == nil {
opts = DefaultConfigCommandConfig()
}

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)
}
summary := summarizeConfig(cmd, internalApp.setupConfig.FangsConfig, opts.makeFilters(internalApp.state.RedactStore), 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 valueFilterFunc func(string) string

type ConfigCommandConfig struct {
LoadConfig bool
IncludeLocationsSubcommand bool
ReplaceHomeDirWithTilde bool
}

func DefaultConfigCommandConfig() *ConfigCommandConfig {
return &ConfigCommandConfig{
IncludeLocationsSubcommand: true,
ReplaceHomeDirWithTilde: true,
}
}

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

// WithReplaceHomeDirWithTilde 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. Note: this does not apply to the
// locations subcommand, only the config command itself
func (c *ConfigCommandConfig) WithReplaceHomeDirWithTilde(replace bool) *ConfigCommandConfig {
c.ReplaceHomeDirWithTilde = replace
return c
}

func (c *ConfigCommandConfig) makeFilters(redactStore redact.Store) (filter valueFilterFunc) {
if redactStore != nil {
filter = chainFilterFuncs(redactStore.RedactString, filter)
}
if c.ReplaceHomeDirWithTilde {
userHome, _ := homedir.Dir()
if userHome != "" {
filter = chainFilterFuncs(filter, 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
})
}
}
return filter
}

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

0 comments on commit d81e109

Please sign in to comment.