diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 271fffe94f1a..5398fa8b07e0 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -51,7 +51,10 @@ func wh(text string) string { return color.GreenString(text) } -const defaultTimeout = time.Minute +const ( + defaultTimeout = time.Minute + defaultRemoteConfigDownloadTimeout = 10 * time.Second +) //nolint:funlen func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, isFinalInit bool) { @@ -104,7 +107,9 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config, m *lintersdb.Manager, is fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)")) fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, wh("Print avg and max memory usage of golangci-lint and total time")) - fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH`")) + fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH` or remote using (should start with http or https)")) + fs.DurationVar(&rc.RemoteConfigDownloadTimeout, "config-download-timeout", defaultRemoteConfigDownloadTimeout, + wh("Timeout for the remote config file download")) fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config")) fs.StringSliceVar(&rc.SkipDirs, "skip-dirs", nil, wh("Regexps of directories to skip")) fs.BoolVar(&rc.UseDefaultSkipDirs, "skip-dirs-use-default", true, getDefaultDirectoryExcludeHelp()) diff --git a/pkg/config/reader.go b/pkg/config/reader.go index 6e97277daa2a..be0cce25da66 100644 --- a/pkg/config/reader.go +++ b/pkg/config/reader.go @@ -1,8 +1,13 @@ package config import ( + "context" "errors" "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" "os" "path/filepath" "strings" @@ -19,6 +24,7 @@ type FileReader struct { log logutils.Log cfg *Config commandLineCfg *Config + remoteConfig bool } func NewFileReader(toCfg, commandLineCfg *Config, log logutils.Log) *FileReader { @@ -43,6 +49,8 @@ func (r *FileReader) Read() error { return fmt.Errorf("can't parse --config option: %s", err) } + defer r.cleanConfigFile(configFile) + if configFile != "" { viper.SetConfigFile(configFile) } else { @@ -212,6 +220,21 @@ func (r *FileReader) parseConfigOption() (string, error) { return "", errConfigDisabled } + r.remoteConfig = r.isRemoteFile(configFile) + + if r.remoteConfig { + r.log.Infof("Provided config file is remote %s", configFile) + + ctx, cancel := context.WithTimeout(context.Background(), r.commandLineCfg.Run.RemoteConfigDownloadTimeout) + defer cancel() + + tmpConfigFile, err := r.downloadFile(ctx, configFile) + if err != nil { + return "", fmt.Errorf("failed to download config file") + } + return tmpConfigFile, nil + } + configFile, err := homedir.Expand(configFile) if err != nil { return "", fmt.Errorf("failed to expand configuration path") @@ -219,3 +242,47 @@ func (r *FileReader) parseConfigOption() (string, error) { return configFile, nil } + +func (r *FileReader) isRemoteFile(configFile string) bool { + u, err := url.Parse(configFile) + return err == nil && u.Scheme != "" && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") +} + +func (r *FileReader) downloadFile(ctx context.Context, configFile string) (string, error) { + tmpfile, err := ioutil.TempFile("", "*.golangci.yml") + if err != nil { + return "", err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, configFile, nil) + if err != nil { + return "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + _, err = io.Copy(tmpfile, resp.Body) + if err != nil { + return "", err + } + + err = tmpfile.Close() + if err != nil { + return "", err + } + + return tmpfile.Name(), nil +} + +func (r *FileReader) cleanConfigFile(configFile string) { + if r.remoteConfig { + err := os.Remove(configFile) + if err != nil { + r.log.Warnf("Can't clean config file: %s", err) + } + } +} diff --git a/pkg/config/reader_test.go b/pkg/config/reader_test.go new file mode 100644 index 000000000000..32121221640a --- /dev/null +++ b/pkg/config/reader_test.go @@ -0,0 +1,49 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsRemoteFile(t *testing.T) { + r := NewFileReader(nil, nil, nil) + tests := []struct { + ConfigFile string + IsRemote bool + }{ + { + ConfigFile: "~/config/.golangcilint.yaml", + IsRemote: false, + }, + { + ConfigFile: "~/http/config/.golangcilint.yaml", + IsRemote: false, + }, + { + ConfigFile: ".golangcilint.yaml", + IsRemote: false, + }, + { + ConfigFile: ".golangcilint.yaml", + IsRemote: false, + }, + { + ConfigFile: "localhost:8080/.golangci.example.yml", + IsRemote: false, // Scheme is mandatory to determine if this is a remote file + }, + { + ConfigFile: "https://raw.githubusercontent.com/golangci/golangci-lint/master/.golangci.example.yml", + IsRemote: true, + }, + { + ConfigFile: "http://localhost:8080/.golangci.example.yml", + IsRemote: true, + }, + } + + for _, test := range tests { + result := r.isRemoteFile(test.ConfigFile) + assert.Equal(t, test.IsRemote, result) + } +} diff --git a/pkg/config/run.go b/pkg/config/run.go index ff6347945e2d..fb8ebb35402e 100644 --- a/pkg/config/run.go +++ b/pkg/config/run.go @@ -11,8 +11,9 @@ type Run struct { Concurrency int PrintResourcesUsage bool `mapstructure:"print-resources-usage"` - Config string - NoConfig bool + Config string + RemoteConfigDownloadTimeout time.Duration `mapstructure:"config-download-timeout"` + NoConfig bool Args []string