diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index d571be509..000000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 HootSuite Media Inc. -// -// 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. -// Modified hereafter by contributors to runatlantis/atlantis. - -// Package cmd provides all CLI commands. -// NOTE: These are different from the commands that get run via pull request -// comments. -package cmd diff --git a/cmd/help_fmt.go b/cmd/help_fmt.go deleted file mode 100644 index b4d560827..000000000 --- a/cmd/help_fmt.go +++ /dev/null @@ -1,137 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "sort" - "strings" - "text/template" -) - -// usageTmpl returns a cobra-compatible usage template that will be printed -// during the help output. -// This template prints help like: -// -// --name= -// -// -// We use it over the default template so that the output it easier to read. -func usageTmpl(stringFlags map[string]stringFlag, intFlags map[string]intFlag, boolFlags map[string]boolFlag) string { - var flagNames []string - for name, f := range stringFlags { - if f.hidden { - continue - } - flagNames = append(flagNames, name) - } - for name, f := range boolFlags { - if f.hidden { - continue - } - flagNames = append(flagNames, name) - } - for name, f := range intFlags { - if f.hidden { - continue - } - flagNames = append(flagNames, name) - } - sort.Strings(flagNames) - - type flag struct { - Name string - Description string - IsBoolFlag bool - } - - var flags []flag - for _, name := range flagNames { - var descrip string - var isBool bool - if f, ok := stringFlags[name]; ok { - descripWithDefault := f.description - if f.defaultValue != "" { - descripWithDefault += fmt.Sprintf(" (default %q)", f.defaultValue) - } - descrip = to80CharCols(descripWithDefault) - isBool = false - } else if f, ok := boolFlags[name]; ok { - descrip = to80CharCols(f.description) - isBool = true - } else if f, ok := intFlags[name]; ok { - descripWithDefault := f.description - if f.defaultValue != 0 { - descripWithDefault += fmt.Sprintf(" (default %d)", f.defaultValue) - } - descrip = to80CharCols(descripWithDefault) - isBool = false - } else { - panic("this is a bug") - } - - flags = append(flags, flag{ - Name: name, - Description: descrip, - IsBoolFlag: isBool, - }) - } - - tmpl := template.Must(template.New("").Parse( - " --{{.Name}}{{if not .IsBoolFlag}}={{end}}\n{{.Description}}\n")) - var flagHelpOutput string - for _, f := range flags { - buf := &bytes.Buffer{} - if err := tmpl.Execute(buf, f); err != nil { - panic(err) - } - flagHelpOutput += buf.String() - } - - // Most of this template is taken from cobra.Command.UsageTemplate() - // but we're subbing out the "Flags:" section with our custom output. - return fmt.Sprintf(`Usage:{{if .Runnable}} - {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} - -Aliases: - {{.NameAndAliases}}{{end}}{{if .HasExample}} - -Examples: -{{.Example}}{{end}}{{if .HasAvailableSubCommands}} - -Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} - -Flags: - -%s{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} - -Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} - -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} -`, flagHelpOutput) -} - -func to80CharCols(s string) string { - var splitAt80 string - splitSpaces := strings.Split(s, " ") - var nextLine string - for i, spaceSplit := range splitSpaces { - if len(nextLine)+len(spaceSplit)+1 > 80 { - splitAt80 += fmt.Sprintf(" %s\n", strings.TrimSuffix(nextLine, " ")) - nextLine = "" - } - if i == len(splitSpaces)-1 { - nextLine += spaceSplit + " " - splitAt80 += fmt.Sprintf(" %s\n", strings.TrimSuffix(nextLine, " ")) - break - } - nextLine += spaceSplit + " " - } - - return splitAt80 -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 5888d4c50..000000000 --- a/cmd/root.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2017 HootSuite Media Inc. -// -// 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. -// Modified hereafter by contributors to runatlantis/atlantis. - -package cmd - -import ( - "os" - - "github.com/spf13/cobra" -) - -// RootCmd is the base command onto which all other commands are added. -var RootCmd = &cobra.Command{ - Use: "atlantis", - Short: "Terraform Pull Request Automation", -} - -// Execute starts RootCmd. -func Execute() { - if err := RootCmd.Execute(); err != nil { - os.Exit(1) - } -} diff --git a/cmd/server.go b/cmd/server.go index 2437f5865..e079441e6 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -17,11 +17,9 @@ import ( "fmt" "net/url" "os" - "path/filepath" - "strings" + "strconv" - "github.com/docker/docker/pkg/fileutils" - homedir "github.com/mitchellh/go-homedir" + "github.com/alecthomas/kong" "github.com/palantir/go-githubapp/githubapp" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server" @@ -33,958 +31,241 @@ import ( "github.com/runatlantis/atlantis/server/neptune/gateway" "github.com/runatlantis/atlantis/server/neptune/temporalworker" neptune "github.com/runatlantis/atlantis/server/neptune/temporalworker/config" - "github.com/spf13/cobra" - "github.com/spf13/viper" ) -// To add a new flag you must: -// 1. Add a const with the flag name (in alphabetic order). -// 2. Add a new field to server.UserConfig and set the mapstructure tag equal to the flag name. -// 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices. -const ( - // Flag names. - ADWebhookPasswordFlag = "azuredevops-webhook-password" // nolint: gosec - ADWebhookUserFlag = "azuredevops-webhook-user" - ADTokenFlag = "azuredevops-token" // nolint: gosec - ADUserFlag = "azuredevops-user" - AtlantisURLFlag = "atlantis-url" - AutoplanFileListFlag = "autoplan-file-list" - BitbucketBaseURLFlag = "bitbucket-base-url" - BitbucketTokenFlag = "bitbucket-token" - BitbucketUserFlag = "bitbucket-user" - BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" - ConfigFlag = "config" - CheckoutStrategyFlag = "checkout-strategy" - DataDirFlag = "data-dir" - DefaultTFVersionFlag = "default-tf-version" - DisableApplyAllFlag = "disable-apply-all" - DisableApplyFlag = "disable-apply" - DisableAutoplanFlag = "disable-autoplan" - DisableMarkdownFoldingFlag = "disable-markdown-folding" - EnableRegExpCmdFlag = "enable-regexp-cmd" - EnableDiffMarkdownFormat = "enable-diff-markdown-format" - FFOwnerFlag = "ff-owner" - FFRepoFlag = "ff-repo" - FFBranchFlag = "ff-branch" - FFPathFlag = "ff-path" - GHHostnameFlag = "gh-hostname" - GHTokenFlag = "gh-token" - GHUserFlag = "gh-user" - GHAppIDFlag = "gh-app-id" - GHAppKeyFlag = "gh-app-key" - GHAppKeyFileFlag = "gh-app-key-file" - GHAppSlugFlag = "gh-app-slug" - GHOrganizationFlag = "gh-org" - GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec - GitlabHostnameFlag = "gitlab-hostname" - GitlabTokenFlag = "gitlab-token" - GitlabUserFlag = "gitlab-user" - GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec - HidePrevPlanComments = "hide-prev-plan-comments" - LogLevelFlag = "log-level" - ParallelPoolSize = "parallel-pool-size" - MaxProjectsPerPR = "max-projects-per-pr" - StatsNamespace = "stats-namespace" - AllowDraftPRs = "allow-draft-prs" - PortFlag = "port" - RepoConfigFlag = "repo-config" - RepoConfigJSONFlag = "repo-config-json" - // RepoWhitelistFlag is deprecated for RepoAllowlistFlag. - RepoWhitelistFlag = "repo-whitelist" - RepoAllowlistFlag = "repo-allowlist" - SlackTokenFlag = "slack-token" - SSLCertFileFlag = "ssl-cert-file" - SSLKeyFileFlag = "ssl-key-file" - TFDownloadURLFlag = "tf-download-url" - VCSStatusName = "vcs-status-name" - WriteGitFileFlag = "write-git-creds" - LyftAuditJobsSnsTopicArnFlag = "lyft-audit-jobs-sns-topic-arn" - LyftGatewaySnsTopicArnFlag = "lyft-gateway-sns-topic-arn" - LyftModeFlag = "lyft-mode" - LyftWorkerQueueURLFlag = "lyft-worker-queue-url" - - // NOTE: Must manually set these as defaults in the setDefaults function. - DefaultADBasicUser = "" - DefaultADBasicPassword = "" - DefaultAutoplanFileList = "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl" - DefaultCheckoutStrategy = "branch" - DefaultCheckrunDetailsURLFlag = "default-checkrun-details-url" - DefaultBitbucketBaseURL = bitbucketcloud.BaseURL - DefaultDataDir = "~/.atlantis" - DefaultGHHostname = "github.com" - DefaultGitlabHostname = "gitlab.com" - DefaultLogLevel = "info" - DefaultParallelPoolSize = 15 - DefaultStatsNamespace = "atlantis" - DefaultPort = 4141 - DefaultTFDownloadURL = "https://releases.hashicorp.com" - DefaultVCSStatusName = "atlantis" -) - -var stringFlags = map[string]stringFlag{ - ADTokenFlag: { - description: "Azure DevOps token of API user. Can also be specified via the ATLANTIS_AZUREDEVOPS_TOKEN environment variable.", - }, - ADUserFlag: { - description: "Azure DevOps username of API user.", - }, - ADWebhookPasswordFlag: { - description: "Azure DevOps basic HTTP authentication password for inbound webhooks " + - "(see https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops)." + - " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from your Azure DevOps org. " + - "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + - "Should be specified via the ATLANTIS_AZUREDEVOPS_WEBHOOK_PASSWORD environment variable.", - defaultValue: "", - }, - ADWebhookUserFlag: { - description: "Azure DevOps basic HTTP authentication username for inbound webhooks.", - defaultValue: "", - }, - AtlantisURLFlag: { - description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ". Supports a base path ex. https://example.com/basepath.", - }, - AutoplanFileListFlag: { - description: "Comma separated list of file patterns that Atlantis will use to check if a directory contains modified files that should trigger project planning." + - " Patterns use the dockerignore (https://docs.docker.com/engine/reference/builder/#dockerignore-file) syntax." + - " Use single quotes to avoid shell expansion of '*'. Defaults to '" + DefaultAutoplanFileList + "'." + - " A custom Workflow that uses autoplan 'when_modified' will ignore this value.", - defaultValue: DefaultAutoplanFileList, - }, - BitbucketUserFlag: { - description: "Bitbucket username of API user.", - }, - BitbucketTokenFlag: { - description: "Bitbucket app password of API user. Can also be specified via the ATLANTIS_BITBUCKET_TOKEN environment variable.", - }, - BitbucketBaseURLFlag: { - description: "Base URL of Bitbucket Server (aka Stash) installation." + - " Must include 'http://' or 'https://'." + - " If using Bitbucket Cloud (bitbucket.org), do not set.", - defaultValue: DefaultBitbucketBaseURL, - }, - BitbucketWebhookSecretFlag: { - description: "Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets." + - " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. " + - "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + - "Should be specified via the ATLANTIS_BITBUCKET_WEBHOOK_SECRET environment variable.", - }, - CheckoutStrategyFlag: { - description: "How to check out pull requests. Accepts either 'branch' (default) or 'merge'." + - " If set to branch, Atlantis will check out the source branch of the pull request." + - " If set to merge, Atlantis will check out the destination branch of the pull request (ex. master)" + - " and then locally perform a git merge of the source branch." + - " This effectively means Atlantis operates on the repo as it will look" + - " after the pull request is merged.", - defaultValue: "branch", - }, - ConfigFlag: { - description: "Path to yaml config file where flag values can also be set.", - }, - DataDirFlag: { - description: "Path to directory to store Atlantis data.", - defaultValue: DefaultDataDir, - }, - DefaultCheckrunDetailsURLFlag: { - description: "URL to redirect to if details URL is not set in the checkrun UI", - }, - FFOwnerFlag: { - description: "Owner of the repo used to house feature flag configuration.", - }, - FFRepoFlag: { - description: "Repo used to house feature flag configuration.", - }, - FFBranchFlag: { - description: "Branch on repo to pull the feature flag configuration.", - }, - FFPathFlag: { - description: "Path in repo to get feature flag configuration.", - }, - GHHostnameFlag: { - description: "Hostname of your Github Enterprise installation. If using github.com, no need to set.", - defaultValue: DefaultGHHostname, - }, - GHUserFlag: { - description: "GitHub username of API user.", - defaultValue: "", - }, - GHTokenFlag: { - description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.", - }, - GHAppKeyFlag: { - description: "The GitHub App's private key", - defaultValue: "", - }, - GHAppKeyFileFlag: { - description: "A path to a file containing the GitHub App's private key", - defaultValue: "", - }, - GHAppSlugFlag: { - description: "The Github app slug (ie. the URL-friendly name of your GitHub App)", - }, - GHOrganizationFlag: { - description: "The name of the GitHub organization to use during the creation of a Github App for Atlantis", - defaultValue: "", - }, - GHWebhookSecretFlag: { - description: "Secret used to validate GitHub webhooks (see https://developer.github.com/webhooks/securing/)." + - " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. " + - "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + - "Should be specified via the ATLANTIS_GH_WEBHOOK_SECRET environment variable.", - }, - GitlabHostnameFlag: { - description: "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.", - defaultValue: DefaultGitlabHostname, - }, - GitlabUserFlag: { - description: "GitLab username of API user.", - }, - GitlabTokenFlag: { - description: "GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.", - }, - GitlabWebhookSecretFlag: { - description: "Optional secret used to validate GitLab webhooks." + - " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. " + - "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + - "Should be specified via the ATLANTIS_GITLAB_WEBHOOK_SECRET environment variable.", - }, - LogLevelFlag: { - description: "Log level. Either debug, info, warn, or error.", - defaultValue: DefaultLogLevel, - }, - StatsNamespace: { - description: "Namespace for aggregating stats.", - defaultValue: DefaultStatsNamespace, - }, - RepoConfigFlag: { - description: "Path to a repo config file, used to customize how Atlantis runs on each repo. See runatlantis.io/docs for more details.", - }, - RepoConfigJSONFlag: { - description: "Specify repo config as a JSON string. Useful if you don't want to write a config file to disk.", - }, - RepoAllowlistFlag: { - description: "Comma separated list of repositories that Atlantis will operate on. " + - "The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any characters until the next comma. Examples: " + - "all repos: '*' (not secure), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'." + - " For Bitbucket Server, {owner} is the name of the project (not the key).", - }, - RepoWhitelistFlag: { - description: "[Deprecated for --repo-allowlist].", - hidden: true, - }, - SlackTokenFlag: { - description: "API token for Slack notifications.", - }, - SSLCertFileFlag: { - description: "File containing x509 Certificate used for serving HTTPS. If the cert is signed by a CA, the file should be the concatenation of the server's certificate, any intermediates, and the CA's certificate.", - }, - SSLKeyFileFlag: { - description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag), - }, - TFDownloadURLFlag: { - description: "Base URL to download Terraform versions from.", - defaultValue: DefaultTFDownloadURL, - }, - DefaultTFVersionFlag: { - description: "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk." + - " If not set, Atlantis uses the terraform binary in its PATH.", - }, - VCSStatusName: { - description: "Name used to identify Atlantis for pull request statuses.", - defaultValue: DefaultVCSStatusName, - }, - LyftAuditJobsSnsTopicArnFlag: { - description: "Provide SNS topic ARN to publish apply workflow's status. Sns topic is used for auditing purposes", - defaultValue: "", - }, - LyftGatewaySnsTopicArnFlag: { - description: "Provide SNS topic ARN to publish GH events to atlantis worker. Sns topic is used in gateway proxy mode", - defaultValue: "", - }, - LyftModeFlag: { - description: "Specifies which mode to run atlantis in. If not set, will assume the default mode. Available modes:\n" + - "default: Runs atlantis with default event handler that processes events within same.\n" + - "gateway: Runs atlantis with gateway event handler that publishes events through sns.\n" + - "worker: Runs atlantis with a sqs handler that polls for events in the queue to process.\n" + - "hybrid: Runs atlantis with both a gateway event handler and sqs handler to perform both gateway and worker behaviors.", - defaultValue: "", - }, - LyftWorkerQueueURLFlag: { - description: "Provide queue of AWS SQS queue for atlantis work to pull GH events from and process.", - defaultValue: "", - }, -} - -var boolFlags = map[string]boolFlag{ - DisableApplyAllFlag: { - description: "Disable \"atlantis apply\" command without any flags (i.e. apply all). A specific project/workspace/directory has to be specified for applies.", - defaultValue: false, - }, - DisableApplyFlag: { - description: "Disable all \"atlantis apply\" command regardless of which flags are passed with it.", - defaultValue: false, - }, - DisableAutoplanFlag: { - description: "Disable atlantis auto planning feature", - defaultValue: false, - }, - EnableRegExpCmdFlag: { - description: "Enable Atlantis to use regular expressions on plan/apply commands when \"-p\" flag is passed with it.", - defaultValue: false, - }, - EnableDiffMarkdownFormat: { - description: "Enable Atlantis to format Terraform plan output into a markdown-diff friendly format for color-coding purposes.", - defaultValue: false, - }, - AllowDraftPRs: { - description: "Enable autoplan for Github Draft Pull Requests", - defaultValue: false, - }, - HidePrevPlanComments: { - description: "Hide previous plan comments to reduce clutter in the PR. " + - "VCS support is limited to: GitHub.", - defaultValue: false, - }, - DisableMarkdownFoldingFlag: { - description: "Toggle off folding in markdown output.", - defaultValue: false, - }, - WriteGitFileFlag: { - description: "Write out a .git-credentials file with the provider user and token to allow cloning private modules over HTTPS or SSH." + - " This writes secrets to disk and should only be enabled in a secure environment.", - defaultValue: false, - }, -} -var intFlags = map[string]intFlag{ - ParallelPoolSize: { - description: "Max size of the wait group that runs parallel plans and applies (if enabled).", - defaultValue: DefaultParallelPoolSize, - }, - MaxProjectsPerPR: { - description: "Max number of projects to operate on in a given pull request.", - defaultValue: events.InfiniteProjectsPerPR, - }, - PortFlag: { - description: "Port to bind to.", - defaultValue: DefaultPort, - }, -} - -var int64Flags = map[string]int64Flag{ - GHAppIDFlag: { - description: "GitHub App Id. If defined, initializes the GitHub client with app-based credentials", - defaultValue: 0, - }, +type Context struct { + Version string } -// ValidLogLevels are the valid log levels that can be set -var ValidLogLevels = []string{"debug", "info", "warn", "error"} +type VersionCmd struct{} -type stringFlag struct { - description string - defaultValue string - hidden bool -} -type intFlag struct { - description string - defaultValue int - hidden bool -} -type int64Flag struct { - description string - defaultValue int64 - hidden bool -} -type boolFlag struct { - description string - defaultValue bool - hidden bool +func (cmd *VersionCmd) Run(ctx Context) error { + fmt.Printf("atlantis %s\n", ctx.Version) + return nil } -// ServerCmd is an abstraction that helps us test. It allows -// us to mock out starting the actual server. type ServerCmd struct { - ServerCreator ServerCreator - Viper *viper.Viper - // SilenceOutput set to true means nothing gets printed. - // Useful for testing to keep the logs clean. - SilenceOutput bool - AtlantisVersion string + server.UserConfig `kong:"embed"` } -func NewServerCmd(v *viper.Viper, version string) *ServerCmd { - return &ServerCmd{ - ServerCreator: &ServerCreatorProxy{ - GatewayCreator: &GatewayCreator{}, - WorkerCreator: &WorkerCreator{}, - TemporalWorkerCreator: &TemporalWorker{}, - }, - Viper: v, - AtlantisVersion: version, - } +var CLI struct { + Version VersionCmd `cmd:"version" help:"Print the current Atlantis version"` + Server ServerCmd `cmd:"server" help:"Start the atlantis server"` } -// ServerStarter is for starting up a server. -// It's an abstraction to help us test. -type ServerStarter interface { - Start() error +var FlagsVars = kong.Vars{ + "help_ad_token": "Azure DevOps token of API user.", + "help_ad_user": "Azure DevOps username of API user.", + "help_ad_webhook_password": "Azure DevOps basic HTTP authentication password for inbound webhooks " + + "(see https://docs.microsoft.com/en-us/azure/devops/service-hooks/authorize?view=azure-devops).\n" + + "SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook " + + "call came from your Azure DevOps org. This means that an attacker could spoof calls to Atlantis " + + "and cause it to perform malicious actions.", + "help_ad_webhook_user": "Azure DevOps basic HTTP authentication username for inbound webhooks.", + "help_atlantis_url": "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port. " + + "Supports a base path ex. https://example.com/basepath.", + "help_autoplan_file_list": "Comma separated list of file patterns that Atlantis will use to check if " + + "a directory contains modified files that should trigger project planning. Patterns use the " + + "dockerignore (https://docs.docker.com/engine/reference/builder/#dockerignore_file) syntax." + + "Use single quotes to avoid shell expansion of '*'. A custom Workflow that uses autoplan " + + "'when_modified' will ignore this value.", + "default_autoplan_file_list": "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + "help_bitbucket_user": "Bitbucket username of API user.", + "help_bitbucket_token": "Bitbucket app password of API user.", + "help_bitbucket_base_url": "Base URL of Bitbucket Server (aka Stash) installation. " + + "Must include 'http://' or 'https://'. If using Bitbucket Cloud (bitbucket.org), do not set.", + "default_bitbucket_base_url": bitbucketcloud.BaseURL, + "help_bitbucket_webhook_secret": "Secret used to validate Bitbucket webhooks. Only Bitbucket Server " + + "supports webhook secrets.\n" + + "SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook " + + "call came from Bitbucket. This means that an attacker could spoof calls to Atlantis and cause it " + + "to perform malicious actions.", + "help_checkout_strategy": "How to check out pull requests. Accepts either 'branch' (default) or 'merge'. " + + "If set to branch, Atlantis will check out the source branch of the pull request. If set to merge, " + + "Atlantis will check out the destination branch of the pull request (ex. master) and then locally " + + "perform a git merge of the source branch. This effectively means Atlantis operates on the repo " + + "as it will look after the pull request is merged.", + "help_config": "Path to yaml config file where flag values can also be set.", + "help_data_dir": "Path to directory to store Atlantis data.", + "help_default_checkrun_details_url": "URL to redirect to if details URL is not set in the checkrun UI.", + "help_ff_owner": "Owner of the repo used to house feature flag configuration.", + "help_ff_repo": "Repo used to house feature flag configuration.", + "help_ff_branch": "Branch on repo to pull the feature flag configuration.", + "help_ff_path": "Path in repo to get feature flag configuration.", + "help_gh_hostname": "Hostname of your Github Enterprise installation. " + + "If using github.com, no need to set.", + "help_gh_user": "GitHub username of API user.", + "help_gh_token": "GitHub token of API user.", + "help_gh_app_key": "The GitHub App's private key", + "help_gh_app_key_file": "A path to a file containing the GitHub App's private key.", + "help_gh_app_slug": "The Github app slug (ie. the URL-friendly name of your GitHub App).", + "help_gh_organization": "The name of the GitHub organization to use during the creation " + + "of a Github App for Atlantis.", + "help_gh_webhook_secret": "Secret used to validate GitHub webhooks " + + "(see https://developer.github.com/webhooks/securing/).\n" + + "SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call " + + "came from GitHub. This means that an attacker could spoof calls to Atlantis " + + "and cause it to perform malicious actions.", + "help_gitlab_hostname": "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.", + "help_gitlab_user": "GitLab username of API user.", + "help_gitlab_token": "GitLab token of API user.", + "help_gitlab_webhook_secret": "Optional secret used to validate GitLab webhooks.\n" + + "SECURITY WARNING: If not specified, Atlantis won't be able to validate that the " + + "incoming webhook call came from GitLab. This means that an attacker could spoof calls to Atlantis " + + "and cause it to perform malicious actions.", + "help_log_level": "Log level. Either debug, info, warn, or error.", + "help_stats_namespace": "Namespace for aggregating stats.", + "help_repo_config": "Path to a repo config file, used to customize how Atlantis runs on each repo. " + + "See runatlantis.io/docs for more details.", + "help_repo_config_json": "Specify repo config as a JSON string. Useful if you don't want " + + "to write a config file to disk.", + "help_repo_allowlist": "Comma separated list of repositories that Atlantis will operate on." + + "The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any " + + "characters until the next comma. Examples:\n" + + "all repos: '*' (not secure),\n" + + "an entire hostname: 'internalgithub.com/*' or\n" + + "an organization: 'github.com/runatlantis/*'.\n" + + "For Bitbucket Server, {owner} is the name of the project (not the key).", + "help_slack_token": "API token for Slack notifications.", + "help_ssl_cert_file": "File containing x509 Certificate used for serving HTTPS. " + + "If the cert is signed by a CA, the file should be the concatenation of the server's certificate, " + + "any intermediates, and the CA's certificate.", + "help_ssl_key_file": "File containing x509 private key.", + "help_tf_download_url": "Base URL to download Terraform versions from.", + "help_default_tf_version": "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk. " + + "If not set, Atlantis uses the terraform binary in its PATH.", + "help_vcs_status_name": "Name used to identify Atlantis for pull request statuses.", + "help_lyft_audit_jobs_sns_topic_arn": "Provide SNS topic ARN to publish apply workflow's status. " + + "Sns topic is used for auditing purposes", + "help_lyft_gateway_sns_topic_arn": "Provide SNS topic ARN to publish GH events to atlantis worker. " + + "Sns topic is used in gateway proxy mode", + "help_lyft_mode": "Specifies which mode to run atlantis in. If not set, will assume the default mode. " + + "Available modes:\n" + + "default: Runs atlantis with default event handler that processes events within same.\n" + + "gateway: Runs atlantis with gateway event handler that publishes events through sns.\n" + + "worker: Runs atlantis with a sqs handler that polls for events in the queue to process.\n" + + "hybrid: Runs atlantis with both a gateway event handler and sqs handler to perform " + + "both gateway and worker behaviors.", + "help_lyft_worker_queue_url": "Provide queue of AWS SQS queue for atlantis work to pull GH events from and process.", + "help_disable_apply_all": "Disable \"atlantis apply\" command without any flags (i.e. apply all). " + + "A specific project/workspace/directory has to be specified for applies.", + "help_disable_apply": "Disable all \"atlantis apply\" command regardless of which flags are passed with it.", + "help_disable_autoplan": "Disable atlantis auto planning feature", + "help_enable_regexp_cmd": "Enable Atlantis to use regular expressions on plan/apply commands " + + "when \"-p\" flag is passed with it.", + "help_enable_diff_markdown_format": "Enable Atlantis to format Terraform plan output into " + + "a markdown-diff friendly format for color-coding purposes.", + "help_enable_policy_checks": "Blocks applies on plans that fail any of the defined conftest policies", + "help_allow_draft_prs": "Enable autoplan for Github Draft Pull Requests", + "help_hide_prev_plan_comments": "Hide previous plan comments to reduce clutter in the PR. " + + "VCS support is limited to: GitHub.", + "help_disable_markdown_folding": "Toggle off folding in markdown output.", + "help_write_git_creds": "Write out a .git-credentials file with the provider user and token to allow " + + "cloning private modules over HTTPS or SSH. " + + "This writes secrets to disk and should only be enabled in a secure environment.", + "help_parallel_pool_size": "Max size of the wait group that runs parallel plans and applies (if enabled).", + "help_max_projects_per_pr": "Max number of projects to operate on in a given pull request.", + "default_max_projects_per_pr": strconv.Itoa(events.InfiniteProjectsPerPR), + "help_port": "Port to bind to.", + "help_gh_app_id": "GitHub App Id. If defined, initializes the GitHub client with app-based credentials", } -// ServerCreator creates servers. -// It's an abstraction to help us test. -type ServerCreator interface { - NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) -} - -type GatewayCreator struct{} - -func (c *GatewayCreator) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { - // For now we just plumb this data through, ideally though we'd have gateway config pretty isolated - // from worker config however this requires more refactoring and can be done later. - appConfig, err := createGHAppConfig(userConfig) - if err != nil { - return nil, err - } - cfg := gateway.Config{ - DataDir: userConfig.DataDir, - AutoplanFileList: userConfig.AutoplanFileList, - AppCfg: appConfig, - RepoAllowList: userConfig.RepoAllowlist, - MaxProjectsPerPR: userConfig.MaxProjectsPerPR, - FFOwner: userConfig.FFOwner, - FFRepo: userConfig.FFRepo, - FFBranch: userConfig.FFBranch, - FFPath: userConfig.FFPath, - GithubHostname: userConfig.GithubHostname, - GithubWebhookSecret: userConfig.GithubWebhookSecret, - GithubAppID: userConfig.GithubAppID, - GithubAppKeyFile: userConfig.GithubAppKeyFile, - GithubAppSlug: userConfig.GithubAppSlug, - GithubStatusName: userConfig.VCSStatusName, - LogLevel: userConfig.ToLogLevel(), - StatsNamespace: userConfig.StatsNamespace, - Port: userConfig.Port, - RepoConfig: userConfig.RepoConfig, - TFDownloadURL: userConfig.TFDownloadURL, - SNSTopicArn: userConfig.LyftGatewaySnsTopicArn, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, - DefaultCheckrunDetailsURL: userConfig.DefaultCheckrunDetailsURL, - } - return gateway.NewServer(cfg) -} - -type WorkerCreator struct{} - -// NewServer returns the real Atlantis server object. -func (d *WorkerCreator) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { - return server.NewServer(userConfig, config) -} - -type TemporalWorker struct{} - -// NewServer returns the real Atlantis server object. -func (t *TemporalWorker) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { - ctxLogger, err := logging.NewLoggerFromLevel(userConfig.ToLogLevel()) - if err != nil { - return nil, errors.Wrap(err, "failed to build context logger") +func (cmd *ServerCmd) Validate() error { + if cmd.UserConfig.GithubSecrets.AppID == 0 && + string(cmd.UserConfig.GithubSecrets.User) == "" && + string(cmd.UserConfig.GitlabSecrets.User) == "" && + string(cmd.UserConfig.BitbucketSecrets.User) == "" && + string(cmd.UserConfig.AzureDevopsSecrets.User) == "" { + return fmt.Errorf("credentials for at least one VCS provider should be defined") } - globalCfg := valid.NewGlobalCfg(userConfig.DataDir) - validator := &cfgParser.ParserValidator{} - if userConfig.RepoConfig != "" { - globalCfg, err = validator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg) - if err != nil { - return nil, errors.Wrapf(err, "parsing %s file", userConfig.RepoConfig) - } - } - parsedURL, err := server.ParseAtlantisURL(userConfig.AtlantisURL) - if err != nil { - return nil, errors.Wrapf(err, - "parsing atlantis url %q", userConfig.AtlantisURL) - } + var err error - // TODO: we should just supply a yaml file with this info and load it directly into the - // app config struct - appConfig, err := createGHAppConfig(userConfig) + err = cmd.UserConfig.SSLSecrets.Validate() if err != nil { - return nil, err - } - - cfg := &neptune.Config{ - AuthCfg: neptune.AuthConfig{ - SslCertFile: userConfig.SSLCertFile, - SslKeyFile: userConfig.SSLKeyFile, - }, - ServerCfg: neptune.ServerConfig{ - URL: parsedURL, - Version: config.AtlantisVersion, - Port: userConfig.Port, - }, - TerraformCfg: neptune.TerraformConfig{ - DefaultVersion: userConfig.DefaultTFVersion, - DownloadURL: userConfig.TFDownloadURL, - LogFilters: globalCfg.TerraformLogFilter, - }, - ValidationConfig: neptune.ValidationConfig{ - DefaultVersion: globalCfg.PolicySets.Version, - Policies: globalCfg.PolicySets, - }, - JobConfig: globalCfg.PersistenceConfig.Jobs, - DeploymentConfig: globalCfg.PersistenceConfig.Deployments, - DataDir: userConfig.DataDir, - TemporalCfg: globalCfg.Temporal, - App: appConfig, - CtxLogger: ctxLogger, - StatsNamespace: userConfig.StatsNamespace, - Metrics: globalCfg.Metrics, - LyftAuditJobsSnsTopicArn: userConfig.LyftAuditJobsSnsTopicArn, - RevisionSetter: globalCfg.RevisionSetter, - } - return temporalworker.NewServer(cfg) -} - -// ServerCreatorProxy creates the correct server based on the mode passed in through user config -type ServerCreatorProxy struct { - GatewayCreator ServerCreator - WorkerCreator ServerCreator - TemporalWorkerCreator ServerCreator -} - -func (d *ServerCreatorProxy) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { - // maybe there's somewhere better to do this - if err := os.MkdirAll(userConfig.DataDir, 0700); err != nil { - return nil, err - } - if userConfig.ToLyftMode() == server.Gateway { - return d.GatewayCreator.NewServer(userConfig, config) - } - - if userConfig.ToLyftMode() == server.TemporalWorker { - return d.TemporalWorkerCreator.NewServer(userConfig, config) - } - - return d.WorkerCreator.NewServer(userConfig, config) -} - -// Init returns the runnable cobra command. -func (s *ServerCmd) Init() *cobra.Command { - c := &cobra.Command{ - Use: "server", - Short: "Start the atlantis server", - Long: `Start the atlantis server and listen for webhook calls.`, - SilenceErrors: true, - SilenceUsage: true, - PreRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error { - return s.preRun() - }), - RunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error { - return s.run() - }), - } - - // Configure viper to accept env vars prefixed with ATLANTIS_ that can be - // used instead of flags. - s.Viper.SetEnvPrefix("ATLANTIS") - s.Viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - s.Viper.AutomaticEnv() - s.Viper.SetTypeByDefaultValue(true) - - c.SetUsageTemplate(usageTmpl(stringFlags, intFlags, boolFlags)) - // If a user passes in an invalid flag, tell them what the flag was. - c.SetFlagErrorFunc(func(c *cobra.Command, err error) error { - s.printErrorf(err) return err - }) - - // Set string flags. - for name, f := range stringFlags { - usage := f.description - if f.defaultValue != "" { - usage = fmt.Sprintf("%s (default %q)", usage, f.defaultValue) - } - c.Flags().String(name, "", usage+"\n") - s.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck - if f.hidden { - c.Flags().MarkHidden(name) // nolint: errcheck - } } - - // Set int flags. - for name, f := range intFlags { - usage := f.description - if f.defaultValue != 0 { - usage = fmt.Sprintf("%s (default %d)", usage, f.defaultValue) - } - c.Flags().Int(name, 0, usage+"\n") - if f.hidden { - c.Flags().MarkHidden(name) // nolint: errcheck - } - s.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck - } - - // Set int64 flags. - for name, f := range int64Flags { - usage := f.description - if f.defaultValue != 0 { - usage = fmt.Sprintf("%s (default %d)", usage, f.defaultValue) - } - c.Flags().Int(name, 0, usage+"\n") - if f.hidden { - c.Flags().MarkHidden(name) // nolint: errcheck - } - s.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck - } - - // Set bool flags. - for name, f := range boolFlags { - c.Flags().Bool(name, f.defaultValue, f.description+"\n") - if f.hidden { - c.Flags().MarkHidden(name) // nolint: errcheck - } - s.Viper.BindPFlag(name, c.Flags().Lookup(name)) // nolint: errcheck - } - - return c -} - -func (s *ServerCmd) preRun() error { - // If passed a config file then try and load it. - configFile := s.Viper.GetString(ConfigFlag) - if configFile != "" { - s.Viper.SetConfigFile(configFile) - if err := s.Viper.ReadInConfig(); err != nil { - return errors.Wrapf(err, "invalid config: reading %s", configFile) - } - } - return nil -} - -func (s *ServerCmd) run() error { - var userConfig server.UserConfig - if err := s.Viper.Unmarshal(&userConfig); err != nil { - return err - } - s.setDefaults(&userConfig) - - logger, err := logging.NewLoggerFromLevel(userConfig.ToLogLevel()) - + err = cmd.UserConfig.AzureDevopsSecrets.Validate() if err != nil { - return errors.Wrap(err, "initializing logger") - } - - if err := s.validate(userConfig, logger); err != nil { return err } - if err := s.setAtlantisURL(&userConfig); err != nil { - return err - } - if err := s.setDataDir(&userConfig); err != nil { - return err - } - if err := s.deprecationWarnings(&userConfig); err != nil { + err = cmd.UserConfig.BitbucketSecrets.Validate() + if err != nil { return err } - s.securityWarnings(&userConfig, logger) - s.trimAtSymbolFromUsers(&userConfig) - - // Legacy code still partially supports other VCS configs - // so GithubAppKeyFile needs to exist to create the githubapp config - appConfig := githubapp.Config{} - if userConfig.GithubAppKeyFile != "" { - appConfig, err = createGHAppConfig(userConfig) - if err != nil { - return err - } - } - - // Config looks good. Start the server. - server, err := s.ServerCreator.NewServer(userConfig, server.Config{ - AtlantisURLFlag: AtlantisURLFlag, - AtlantisVersion: s.AtlantisVersion, - DefaultTFVersionFlag: DefaultTFVersionFlag, - RepoConfigJSONFlag: RepoConfigJSONFlag, - AppCfg: appConfig, - }) + err = cmd.UserConfig.GithubSecrets.Validate() if err != nil { - return errors.Wrap(err, "initializing server") - } - return server.Start() -} - -func (s *ServerCmd) setDefaults(c *server.UserConfig) { - if c.AutoplanFileList == "" { - c.AutoplanFileList = DefaultAutoplanFileList - } - if c.CheckoutStrategy == "" { - c.CheckoutStrategy = DefaultCheckoutStrategy - } - if c.DataDir == "" { - c.DataDir = DefaultDataDir - } - if c.GithubHostname == "" { - c.GithubHostname = DefaultGHHostname - } - if c.GitlabHostname == "" { - c.GitlabHostname = DefaultGitlabHostname - } - if c.BitbucketBaseURL == "" { - c.BitbucketBaseURL = DefaultBitbucketBaseURL - } - if c.LogLevel == "" { - c.LogLevel = DefaultLogLevel - } - if c.ParallelPoolSize == 0 { - c.ParallelPoolSize = DefaultParallelPoolSize - } - if c.StatsNamespace == "" { - c.StatsNamespace = DefaultStatsNamespace - } - if c.Port == 0 { - c.Port = DefaultPort - } - if c.TFDownloadURL == "" { - c.TFDownloadURL = DefaultTFDownloadURL - } - if c.VCSStatusName == "" { - c.VCSStatusName = DefaultVCSStatusName - } - if c.MaxProjectsPerPR == 0 { - c.MaxProjectsPerPR = events.InfiniteProjectsPerPR - } -} - -func (s *ServerCmd) validate(userConfig server.UserConfig, logger logging.Logger) error { - userConfig.LogLevel = strings.ToLower(userConfig.LogLevel) - if !isValidLogLevel(userConfig.LogLevel) { - return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels) - } - - checkoutStrategy := userConfig.CheckoutStrategy - if checkoutStrategy != "branch" && checkoutStrategy != "merge" { - return errors.New("invalid checkout strategy: not one of branch or merge") - } - - if (userConfig.SSLKeyFile == "") != (userConfig.SSLCertFile == "") { - return fmt.Errorf("--%s and --%s are both required for ssl", SSLKeyFileFlag, SSLCertFileFlag) - } - - // The following combinations are valid. - // 1. github user and token set - // 2. github app ID and (key file set or key set) - // 3. gitlab user and token set - // 4. bitbucket user and token set - // 5. azuredevops user and token set - // 6. any combination of the above - vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GHAppIDFlag, GHAppKeyFileFlag, GHAppIDFlag, GHAppKeyFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag) - if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) || ((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) || ((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "")) { - return vcsErr - } - if (userConfig.GithubAppID != 0) && ((userConfig.GithubAppKey == "") && (userConfig.GithubAppKeyFile == "")) { - return vcsErr - } - if (userConfig.GithubAppID == 0) && ((userConfig.GithubAppKey != "") || (userConfig.GithubAppKeyFile != "")) { - return vcsErr - } - // At this point, we know that there can't be a single user/token without - // its partner, but we haven't checked if any user/token is set at all. - if userConfig.GithubAppID == 0 && userConfig.GithubUser == "" && userConfig.GitlabUser == "" && userConfig.BitbucketUser == "" && userConfig.AzureDevopsUser == "" { - return vcsErr - } - - // Handle deprecation of repo whitelist. - if userConfig.RepoWhitelist == "" && userConfig.RepoAllowlist == "" { - return fmt.Errorf("--%s must be set for security purposes", RepoAllowlistFlag) - } - if userConfig.RepoAllowlist != "" && userConfig.RepoWhitelist != "" { - return fmt.Errorf("both --%s and --%s cannot be set–use --%s", RepoAllowlistFlag, RepoWhitelistFlag, RepoAllowlistFlag) - } - if strings.Contains(userConfig.RepoWhitelist, "://") { - return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoWhitelistFlag) - } - if strings.Contains(userConfig.RepoAllowlist, "://") { - return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoAllowlistFlag) - } - - if userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret != "" { - return fmt.Errorf("--%s cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", BitbucketWebhookSecretFlag) + return err } - - parsed, err := url.Parse(userConfig.BitbucketBaseURL) + err = cmd.UserConfig.GitlabSecrets.Validate() if err != nil { - return fmt.Errorf("error parsing --%s flag value %q: %s", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err) - } - if parsed.Scheme != "http" && parsed.Scheme != "https" { - return fmt.Errorf("--%s must have http:// or https://, got %q", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL) - } - - if userConfig.RepoConfig != "" && userConfig.RepoConfigJSON != "" { - return fmt.Errorf("cannot use --%s and --%s at the same time", RepoConfigFlag, RepoConfigJSONFlag) - } - - // Warn if any tokens have newlines. - for name, token := range map[string]string{ - GHTokenFlag: userConfig.GithubToken, - GHWebhookSecretFlag: userConfig.GithubWebhookSecret, - GitlabTokenFlag: userConfig.GitlabToken, - GitlabWebhookSecretFlag: userConfig.GitlabWebhookSecret, - BitbucketTokenFlag: userConfig.BitbucketToken, - BitbucketWebhookSecretFlag: userConfig.BitbucketWebhookSecret, - } { - if strings.Contains(token, "\n") { - logger.Warn(fmt.Sprintf("--%s contains a newline which is usually unintentional", name)) - } + return err } - - _, patternErr := fileutils.NewPatternMatcher(strings.Split(userConfig.AutoplanFileList, ",")) - if patternErr != nil { - return errors.Wrapf(patternErr, "invalid pattern in --%s, %s", AutoplanFileListFlag, userConfig.AutoplanFileList) + if cmd.UserConfig.RepoConfig != "" && cmd.UserConfig.RepoConfigJSON != "" { + return fmt.Errorf("cannot set both path to repo config and repo config json at the same time") } return nil } -// setAtlantisURL sets the externally accessible URL for atlantis. -func (s *ServerCmd) setAtlantisURL(userConfig *server.UserConfig) error { - if userConfig.AtlantisURL == "" { +func (cmd *ServerCmd) Run(ctx Context) error { + var err error + cmd.AtlantisVersion = ctx.Version + if cmd.UserConfig.AtlantisURL.String() == "" { hostname, err := os.Hostname() if err != nil { return errors.Wrap(err, "failed to determine hostname") } - userConfig.AtlantisURL = fmt.Sprintf("http://%s:%d", hostname, userConfig.Port) - } - return nil -} - -// setDataDir checks if ~ was used in data-dir and converts it to the actual -// home directory. If we don't do this, we'll create a directory called "~" -// instead of actually using home. It also converts relative paths to absolute. -func (s *ServerCmd) setDataDir(userConfig *server.UserConfig) error { - finalPath := userConfig.DataDir - - // Convert ~ to the actual home dir. - if strings.HasPrefix(finalPath, "~/") { - var err error - finalPath, err = homedir.Expand(finalPath) + atlantisUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", hostname, cmd.Port)) if err != nil { - return errors.Wrap(err, "determining home directory") + return err } + cmd.UserConfig.AtlantisURL = server.HttpUrl{atlantisUrl} } - - // Convert relative paths to absolute. - finalPath, err := filepath.Abs(finalPath) - if err != nil { - return errors.Wrap(err, "making data-dir absolute") - } - userConfig.DataDir = finalPath - return nil -} - -// trimAtSymbolFromUsers trims @ from the front of the github and gitlab usernames -func (s *ServerCmd) trimAtSymbolFromUsers(userConfig *server.UserConfig) { - userConfig.GithubUser = strings.TrimPrefix(userConfig.GithubUser, "@") - userConfig.GitlabUser = strings.TrimPrefix(userConfig.GitlabUser, "@") - userConfig.BitbucketUser = strings.TrimPrefix(userConfig.BitbucketUser, "@") - userConfig.AzureDevopsUser = strings.TrimPrefix(userConfig.AzureDevopsUser, "@") -} - -func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig, logger logging.Logger) { - if userConfig.GithubUser != "" && userConfig.GithubWebhookSecret == "" && !s.SilenceOutput { - logger.Warn("no GitHub webhook secret set. This could allow attackers to spoof requests from GitHub") - } - if userConfig.GitlabUser != "" && userConfig.GitlabWebhookSecret == "" && !s.SilenceOutput { - logger.Warn("no GitLab webhook secret set. This could allow attackers to spoof requests from GitLab") - } - if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == "" && !s.SilenceOutput { - logger.Warn("no Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket") - } - if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput { - logger.Warn("Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are allowing only Bitbucket IPs") - } - if userConfig.AzureDevopsWebhookUser != "" && userConfig.AzureDevopsWebhookPassword == "" && !s.SilenceOutput { - logger.Warn("no Azure DevOps webhook user and password set. This could allow attackers to spoof requests from Azure DevOps.") - } -} - -// deprecationWarnings prints a warning if flags that are deprecated are -// being used. Right now this only applies to flags that have been made obsolete -// due to server-side config. -func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error { - var applyReqs []string - var deprecatedFlags []string - - // Build up strings with what the recommended yaml and json config should - // be instead of using the deprecated flags. - yamlCfg := "---\nrepos:\n- id: /.*/" - jsonCfg := `{"repos":[{"id":"/.*/"` - if len(applyReqs) > 0 { - yamlCfg += fmt.Sprintf("\n apply_requirements: [%s]", strings.Join(applyReqs, ", ")) - jsonCfg += fmt.Sprintf(`, "apply_requirements":["%s"]`, strings.Join(applyReqs, "\", \"")) - } - jsonCfg += "}]}" - - if len(deprecatedFlags) > 0 { - warning := "WARNING: " - if len(deprecatedFlags) == 1 { - warning += fmt.Sprintf("Flag --%s has been deprecated.", deprecatedFlags[0]) - } else { - warning += fmt.Sprintf("Flags --%s and --%s have been deprecated.", strings.Join(deprecatedFlags[0:len(deprecatedFlags)-1], ", --"), deprecatedFlags[len(deprecatedFlags)-1:][0]) + // Legacy code still partially supports other VCS configs + // so GithubAppKeyFile needs to exist to create the githubapp config + appConfig := githubapp.Config{} + if cmd.GithubSecrets.AppKeyFile != "" { + appConfig, err = cmd.createGHAppConfig() + if err != nil { + return err } - warning += fmt.Sprintf("\nCreate a --%s file with the following config instead:\n\n%s\n\nor use --%s='%s'\n", - RepoConfigFlag, - yamlCfg, - RepoConfigJSONFlag, - jsonCfg, - ) - fmt.Println(warning) - } - - if userConfig.RepoWhitelist != "" { - userConfig.RepoAllowlist = userConfig.RepoWhitelist } - return nil -} - -// withErrPrint prints out any cmd errors to stderr. -func (s *ServerCmd) withErrPrint(f func(*cobra.Command, []string) error) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - err := f(cmd, args) - if err != nil && !s.SilenceOutput { - s.printErrorf(err) - } + // Config looks good. Start the server. + if err := os.MkdirAll(cmd.DataDir, 0700); err != nil { return err } -} + var srv Server + switch cmd.LyftMode { + case server.Gateway: + srv, err = cmd.NewGatewayServer(appConfig) + case server.TemporalWorker: + srv, err = cmd.NewTemporalWorkerServer(appConfig) + default: + srv, err = cmd.NewWorkerServer(appConfig) + } + if err != nil { + return errors.Wrap(err, "initializing server") + } -// printErr prints err to stderr using a red terminal colour. -func (s *ServerCmd) printErrorf(err error) { - fmt.Fprintf(os.Stderr, "%sError: %s%s\n", "\033[31m", err.Error(), "\033[39m") + return srv.Start() } -func isValidLogLevel(level string) bool { - for _, logLevel := range ValidLogLevels { - if logLevel == level { - return true - } - } - - return false +type Server interface { + Start() error } -func createGHAppConfig(userConfig server.UserConfig) (githubapp.Config, error) { - privateKey, err := os.ReadFile(userConfig.GithubAppKeyFile) - if err != nil { - return githubapp.Config{}, err - } +func (cmd *ServerCmd) createGHAppConfig() (githubapp.Config, error) { return githubapp.Config{ App: struct { IntegrationID int64 "yaml:\"integration_id\" json:\"integrationId\"" WebhookSecret string "yaml:\"webhook_secret\" json:\"webhookSecret\"" PrivateKey string "yaml:\"private_key\" json:\"privateKey\"" }{ - IntegrationID: userConfig.GithubAppID, - WebhookSecret: userConfig.GithubWebhookSecret, - PrivateKey: string(privateKey), + IntegrationID: cmd.UserConfig.GithubSecrets.AppID, + WebhookSecret: cmd.UserConfig.GithubSecrets.WebhookSecret, + PrivateKey: cmd.UserConfig.GithubSecrets.AppKeyFile, }, //TODO: parameterize this @@ -993,3 +274,97 @@ func createGHAppConfig(userConfig server.UserConfig) (githubapp.Config, error) { V4APIURL: "https://api.github.com/graphql", }, nil } + +func (cmd *ServerCmd) NewGatewayServer(appCfg githubapp.Config) (Server, error) { + // For now we just plumb this data through, ideally though we'd have gateway config pretty isolated + // from worker config however this requires more refactoring and can be done later. + + cfg := cmd.UserConfig + allowlist := []string{} + for _, expr := range cfg.RepoAllowlist { + allowlist = append(allowlist, expr.String()) + } + + neptuneCfg := gateway.Config{ + DataDir: cfg.DataDir, + AutoplanFileList: cfg.AutoplanFileList.PatternMatcher, + AppCfg: appCfg, + RepoAllowlist: allowlist, + MaxProjectsPerPR: cfg.MaxProjectsPerPR, + FFOwner: cfg.FFOwner, + FFRepo: cfg.FFRepo, + FFBranch: cfg.FFBranch, + FFPath: cfg.FFPath, + GithubHostname: cfg.GithubSecrets.Hostname.String(), + GithubWebhookSecret: cfg.GithubSecrets.WebhookSecret, + GithubAppID: cfg.GithubSecrets.AppID, + GithubAppKeyFile: cfg.GithubSecrets.AppKeyFile, + GithubAppSlug: cfg.GithubSecrets.AppSlug, + GithubStatusName: cfg.VCSStatusName, + LogLevel: cfg.LogLevel, + StatsNamespace: cfg.StatsNamespace, + Port: cfg.Port, + RepoConfig: cfg.RepoConfig, + TFDownloadURL: cfg.TFDownloadURL.String(), + SNSTopicArn: cfg.LyftGatewaySnsTopicArn, + SSLKeyFile: cfg.SSLSecrets.KeyFile, + SSLCertFile: cfg.SSLSecrets.CertFile, + DefaultCheckrunDetailsURL: cfg.DefaultCheckrunDetailsURL.String(), + } + return gateway.NewServer(neptuneCfg) +} + +// NewWorkerServer returns the real Atlantis server object. +func (cmd *ServerCmd) NewWorkerServer(appCfg githubapp.Config) (Server, error) { + return server.NewServer(cmd.UserConfig, appCfg) +} + +// NewServer returns the real Atlantis server object. +func (cmd *ServerCmd) NewTemporalWorkerServer(appCfg githubapp.Config) (Server, error) { + cfg := cmd.UserConfig + ctxLogger, err := logging.NewLoggerFromLevel(cfg.LogLevel) + if err != nil { + return nil, errors.Wrap(err, "failed to build context logger") + } + + globalCfg := valid.NewGlobalCfg(cfg.DataDir) + validator := &cfgParser.ParserValidator{} + if cfg.RepoConfig != "" { + globalCfg, err = validator.ParseGlobalCfg(cfg.RepoConfig, globalCfg) + if err != nil { + return nil, errors.Wrapf(err, "parsing %s file", cfg.RepoConfig) + } + } + + neptuneCfg := &neptune.Config{ + AuthCfg: neptune.AuthConfig{ + SslCertFile: cfg.SSLSecrets.CertFile, + SslKeyFile: cfg.SSLSecrets.KeyFile, + }, + ServerCfg: neptune.ServerConfig{ + URL: cfg.AtlantisURL.URL, + Version: cfg.AtlantisVersion, + Port: cfg.Port, + }, + TerraformCfg: neptune.TerraformConfig{ + DefaultVersion: cfg.DefaultTFVersion, + DownloadURL: cfg.TFDownloadURL.String(), + LogFilters: globalCfg.TerraformLogFilter, + }, + ValidationConfig: neptune.ValidationConfig{ + DefaultVersion: globalCfg.PolicySets.Version, + Policies: globalCfg.PolicySets, + }, + JobConfig: globalCfg.PersistenceConfig.Jobs, + DeploymentConfig: globalCfg.PersistenceConfig.Deployments, + DataDir: cfg.DataDir, + TemporalCfg: globalCfg.Temporal, + App: appCfg, + CtxLogger: ctxLogger, + StatsNamespace: cfg.StatsNamespace, + Metrics: globalCfg.Metrics, + LyftAuditJobsSnsTopicArn: cfg.LyftAuditJobsSnsTopicArn, + RevisionSetter: globalCfg.RevisionSetter, + } + return temporalworker.NewServer(neptuneCfg) +} diff --git a/cmd/server_test.go b/cmd/server_test.go index 102379380..f29f963bb 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -15,281 +15,477 @@ package cmd import ( "fmt" - "github.com/stretchr/testify/assert" + "net/url" "os" "path/filepath" - "reflect" "strings" "testing" + "github.com/alecthomas/kong" + "github.com/stretchr/testify/assert" + homedir "github.com/mitchellh/go-homedir" "github.com/runatlantis/atlantis/server" "github.com/runatlantis/atlantis/server/events/vcs/fixtures" + "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) -// passedConfig is set to whatever config ended up being passed to NewServer. -// Used for testing. -var passedConfig server.UserConfig +const atlantisVersion = "test-version" -type ServerCreatorMock struct{} +// Adding a new flag? Add it to this slice for testing in alphabetical +// order. -func (s *ServerCreatorMock) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { - passedConfig = userConfig - return &ServerStarterMock{}, nil +type flagValue struct { + Input interface{} + Output interface{} } -type ServerStarterMock struct{} - -func (s *ServerStarterMock) Start() error { - return nil +var testFlags = map[string]flagValue{ + "azuredevops-token": { + Input: "ad-token", + Output: "ad-token", + }, + "azuredevops-user": { + Input: "ad-user", + Output: server.User("ad-user"), + }, + "azuredevops-webhook-password": { + Input: "ad-wh-pass", + Output: "ad-wh-pass", + }, + "azuredevops-webhook-user": { + Input: "ad-wh-user", + Output: "ad-wh-user", + }, + "atlantis-url": { + Input: "http://url", + Output: server.HttpUrl{ + &url.URL{ + Host: "url", + Scheme: "http", + }, + }, + }, + "bitbucket-base-url": { + Input: "https://bitbucket-base-url.com", + Output: server.HttpUrl{ + &url.URL{ + Scheme: "https", + Host: "bitbucket-base-url.com", + }, + }, + }, + "bitbucket-token": { + Input: "bitbucket-token", + Output: "bitbucket-token", + }, + "bitbucket-user": { + Input: "bitbucket-user", + Output: server.User("bitbucket-user"), + }, + "bitbucket-webhook-secret": { + Input: "bitbucket-secret", + Output: "bitbucket-secret", + }, + "checkout-strategy": { + Input: "merge", + Output: "merge", + }, + "data-dir": { + Input: "/path", + Output: "/path", + }, + "default-tf-version": { + Input: "v0.11.0", + Output: "v0.11.0", + }, + "disable-apply-all": { + Input: true, + Output: true, + }, + "disable-apply": { + Input: true, + Output: true, + }, + "disable-markdown-folding": { + Input: true, + Output: true, + }, + "gh-hostname": { + Input: "ghhostname", + Output: server.Schemeless{ + &url.URL{ + Host: "ghhostname", + }, + }, + }, + "gh-token": { + Input: "token", + Output: "token", + }, + "gh-user": { + Input: "user", + Output: server.User("user"), + }, + "gh-app-slug": { + Input: "atlantis", + Output: "atlantis", + }, + "gh-webhook-secret": { + Input: "secret", + Output: "secret", + }, + "gitlab-hostname": { + Input: "gitlab-hostname", + Output: server.Schemeless{ + &url.URL{ + Scheme: "", + Host: "gitlab-hostname", + }, + }, + }, + "gitlab-token": { + Input: "gitlab-token", + Output: "gitlab-token", + }, + "gitlab-user": { + Input: "gitlab-user", + Output: server.User("gitlab-user"), + }, + "gitlab-webhook-secret": { + Input: "gitlab-secret", + Output: "gitlab-secret", + }, + "log-level": { + Input: "debug", + Output: logging.Debug, + }, + "stats-namespace": { + Input: "atlantis", + Output: "atlantis", + }, + "allow-draft-prs": { + Input: true, + Output: true, + }, + "parallel-pool-size": { + Input: 100, + Output: 100, + }, + "repo-allowlist": { + Input: "github.com/runatlantis/atlantis", + Output: []server.Schemeless{{ + &url.URL{ + Host: "github.com", + Path: "/runatlantis/atlantis", + }, + }}, + }, + "slack-token": { + Input: "slack-token", + Output: "slack-token", + }, + "ssl-cert-file": { + Input: "cert-file", + Output: "cert-file", + }, + "ssl-key-file": { + Input: "key-file", + Output: "key-file", + }, + "tf-download-url": { + Input: "https://my-hostname.com", + Output: server.HttpUrl{ + &url.URL{ + Scheme: "https", + Host: "my-hostname.com", + }, + }, + }, + "vcs-status-name": { + Input: "my-status", + Output: "my-status", + }, + "write-git-creds": { + Input: true, + Output: true, + }, + "disable-autoplan": { + Input: true, + Output: true, + }, } -// Adding a new flag? Add it to this slice for testing in alphabetical -// order. -var testFlags = map[string]interface{}{ - ADTokenFlag: "ad-token", - ADUserFlag: "ad-user", - ADWebhookPasswordFlag: "ad-wh-pass", - ADWebhookUserFlag: "ad-wh-user", - AtlantisURLFlag: "url", - AutoplanFileListFlag: "**/*.tf,**/*.yml", - BitbucketBaseURLFlag: "https://bitbucket-base-url.com", - BitbucketTokenFlag: "bitbucket-token", - BitbucketUserFlag: "bitbucket-user", - BitbucketWebhookSecretFlag: "bitbucket-secret", - CheckoutStrategyFlag: "merge", - DataDirFlag: "/path", - DefaultTFVersionFlag: "v0.11.0", - DisableApplyAllFlag: true, - DisableApplyFlag: true, - DisableMarkdownFoldingFlag: true, - GHHostnameFlag: "ghhostname", - GHTokenFlag: "token", - GHUserFlag: "user", - GHAppIDFlag: int64(0), - GHAppKeyFileFlag: "", - GHAppSlugFlag: "atlantis", - GHOrganizationFlag: "", - GHWebhookSecretFlag: "secret", - GitlabHostnameFlag: "gitlab-hostname", - GitlabTokenFlag: "gitlab-token", - GitlabUserFlag: "gitlab-user", - GitlabWebhookSecretFlag: "gitlab-secret", - LogLevelFlag: "debug", - StatsNamespace: "atlantis", - AllowDraftPRs: true, - PortFlag: 8181, - ParallelPoolSize: 100, - RepoAllowlistFlag: "github.com/runatlantis/atlantis", - SlackTokenFlag: "slack-token", - SSLCertFileFlag: "cert-file", - SSLKeyFileFlag: "key-file", - TFDownloadURLFlag: "https://my-hostname.com", - VCSStatusName: "my-status", - WriteGitFileFlag: true, - LyftAuditJobsSnsTopicArnFlag: "", - LyftGatewaySnsTopicArnFlag: "", - LyftModeFlag: "", - LyftWorkerQueueURLFlag: "", - DisableAutoplanFlag: true, - EnableRegExpCmdFlag: false, - EnableDiffMarkdownFormat: false, -} - -func TestExecute_Defaults(t *testing.T) { +func TestRun_Defaults(t *testing.T) { t.Log("Should set the defaults for all unspecified flags.") - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoAllowlistFlag: "*", + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "gh-hostname": { + Input: "ghhostname", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) + /* + * - // Get our hostname since that's what atlantis-url gets defaulted to. - hostname, err := os.Hostname() - Ok(t, err) + // Get our hostname since that's what atlantis-url gets defaulted to. + hostname, err := os.Hostname() + Ok(t, err) - // Get our home dir since that's what data-dir defaulted to. - dataDir, err := homedir.Expand("~/.atlantis") - Ok(t, err) + // Get our home dir since that's what data-dir defaulted to. + dataDir, err := homedir.Expand("~/.atlantis") + Ok(t, err) - strExceptions := map[string]string{ - GHUserFlag: "user", - GHTokenFlag: "token", - DataDirFlag: dataDir, - AtlantisURLFlag: "http://" + hostname + ":4141", - RepoAllowlistFlag: "*", - } - strIgnore := map[string]bool{ - "config": true, - } - for flag, cfg := range stringFlags { - t.Log(flag) - if _, ok := strIgnore[flag]; ok { - continue - } else if excep, ok := strExceptions[flag]; ok { - Equals(t, excep, configVal(t, passedConfig, flag)) - } else { - Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) - } - } - for flag, cfg := range boolFlags { - t.Log(flag) - Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) - } - for flag, cfg := range intFlags { - t.Log(flag) - Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) - } + strExceptions := map[string]string{ + "gh-user": "user", + "gh-token": "token", + "data-dir": dataDir, + "atlantis_url": "http://" + hostname + ":4141", + "repo-allowlist": "*", + } + strIgnore := map[string]bool{ + "config": true, + } + + for flag, cfg := range stringFlags { + t.Log(flag) + if _, ok := strIgnore[flag]; ok { + continue + } else if excep, ok := strExceptions[flag]; ok { + Equals(t, excep, configVal(t, passedConfig, flag)) + } else { + Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) + } + } + for flag, cfg := range boolFlags { + t.Log(flag) + Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) + } + for flag, cfg := range intFlags { + t.Log(flag) + Equals(t, cfg.defaultValue, configVal(t, passedConfig, flag)) + } + */ } -func TestExecute_Flags(t *testing.T) { +func TestRun_Flags(t *testing.T) { t.Log("Should use all flags that are set.") - c := setup(testFlags, t) - err := c.Execute() + c, err := setup(testFlags, t) Ok(t, err) for flag, exp := range testFlags { - Equals(t, exp, configVal(t, passedConfig, flag)) + Equals(t, exp.Output, configVal(t, c, flag)) } } -func TestExecute_GHAppKeyFile(t *testing.T) { +func TestRun_GHAppKeyFile(t *testing.T) { t.Log("Should use all the values from the config file.") tmpFile := tempFile(t, "testdata") defer os.Remove(tmpFile) // nolint: errcheck - c := setup(map[string]interface{}{ - GHAppKeyFileFlag: tmpFile, - GHAppIDFlag: int64(1), - RepoAllowlistFlag: "*", + _, err := setup(map[string]flagValue{ + "gh-app-key-file": { + Input: tmpFile, + }, + "gh-app-id": { + Input: "1", + }, + "gh-hostname": { + Input: "ghhostname", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() assert.NoError(t, err) } -func TestExecute_ConfigFile(t *testing.T) { +func TestRun_ConfigFile(t *testing.T) { t.Log("Should use all the values from the config file.") // Use yaml package to quote values that need quoting - cfgContents, yamlErr := yaml.Marshal(&testFlags) + cfg := make(map[string]map[string]interface{}) + cfg["server"] = make(map[string]interface{}) + for flag, val := range testFlags { + cfg["server"][flag] = val.Input + } + cfgContents, yamlErr := yaml.Marshal(&cfg) Ok(t, yamlErr) tmpFile := tempFile(t, string(cfgContents)) defer os.Remove(tmpFile) // nolint: errcheck - c := setup(map[string]interface{}{ - ConfigFlag: tmpFile, + c, err := setup(map[string]flagValue{ + "config": { + Input: tmpFile, + }, }, t) - err := c.Execute() Ok(t, err) for flag, exp := range testFlags { - Equals(t, exp, configVal(t, passedConfig, flag)) + Equals(t, exp.Output, configVal(t, c, flag)) } } -func TestExecute_EnvironmentVariables(t *testing.T) { +func TestRun_EnvironmentVariables(t *testing.T) { t.Log("Environment variables should work.") for flag, value := range testFlags { envKey := "ATLANTIS_" + strings.ToUpper(strings.ReplaceAll(flag, "-", "_")) - os.Setenv(envKey, fmt.Sprintf("%v", value)) // nolint: errcheck + envVal := "" + switch value.Input.(type) { + case string: + envVal = value.Input.(string) + case bool: + envVal = fmt.Sprintf("%t", value.Input.(bool)) + case int: + envVal = fmt.Sprintf("%d", value.Input.(int)) + } + os.Setenv(envKey, envVal) // nolint: errcheck defer func(key string) { os.Unsetenv(key) }(envKey) } - c := setup(nil, t) - err := c.Execute() + c, err := setup(nil, t) Ok(t, err) for flag, exp := range testFlags { - Equals(t, exp, configVal(t, passedConfig, flag)) + Equals(t, exp.Output, configVal(t, c, flag)) } } -func TestExecute_NoConfigFlag(t *testing.T) { - t.Log("If there is no config flag specified Execute should return nil.") - c := setupWithDefaults(map[string]interface{}{ - ConfigFlag: "", +func TestRun_NoConfigFlag(t *testing.T) { + t.Log("If there is no config flag specified Run should return nil.") + _, err := setup(map[string]flagValue{ + "config": { + Input: "", + }, + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "gh-hostname": { + Input: "ghhostname", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) } -func TestExecute_ConfigFileExtension(t *testing.T) { +func TestRun_ConfigFileExtension(t *testing.T) { t.Log("If the config file doesn't have an extension then error.") - c := setupWithDefaults(map[string]interface{}{ - ConfigFlag: "does-not-exist", + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "config": { + Input: "does-not-exist", + }, }, t) - err := c.Execute() - Equals(t, "invalid config: reading does-not-exist: Unsupported Config Type \"\"", err.Error()) + Equals(t, "no loader for config with extension \"\" found", err.Error()) } -func TestExecute_ConfigFileMissing(t *testing.T) { +func TestRun_ConfigFileMissing(t *testing.T) { t.Log("If the config file doesn't exist then error.") - c := setupWithDefaults(map[string]interface{}{ - ConfigFlag: "does-not-exist.yaml", + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "gh-hostname": { + Input: "ghhostname", + }, + "config": { + Input: "does-not-exist.yaml", + }, }, t) - err := c.Execute() - Equals(t, "invalid config: reading does-not-exist.yaml: open does-not-exist.yaml: no such file or directory", err.Error()) + p, _ := os.Getwd() + Equals(t, fmt.Sprintf("open %s/does-not-exist.yaml: no such file or directory", p), err.Error()) } -func TestExecute_ConfigFileExists(t *testing.T) { +func TestRun_ConfigFileExists(t *testing.T) { t.Log("If the config file exists then there should be no error.") - tmpFile := tempFile(t, "") + tmpFile := tempFile(t, "---") defer os.Remove(tmpFile) // nolint: errcheck - c := setupWithDefaults(map[string]interface{}{ - ConfigFlag: tmpFile, + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "gh-hostname": { + Input: "ghhostname", + }, + "repo-allowlist": { + Input: "*", + }, + "config": { + Input: tmpFile, + }, }, t) - err := c.Execute() Ok(t, err) } -func TestExecute_InvalidConfig(t *testing.T) { +func TestRun_InvalidConfig(t *testing.T) { t.Log("If the config file contains invalid yaml there should be an error.") tmpFile := tempFile(t, "invalidyaml") defer os.Remove(tmpFile) // nolint: errcheck - c := setupWithDefaults(map[string]interface{}{ - ConfigFlag: tmpFile, + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "config": { + Input: tmpFile, + }, }, t) - err := c.Execute() Assert(t, strings.Contains(err.Error(), "unmarshal errors"), "should be an unmarshal error") } -// Should error if the repo allowlist contained a scheme. -func TestExecute_RepoAllowlistScheme(t *testing.T) { - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoAllowlistFlag: "http://github.com/*", - }, t) - err := c.Execute() - Assert(t, err != nil, "should be an error") - Equals(t, "--repo-allowlist cannot contain ://, should be hostnames only", err.Error()) -} - -func TestExecute_ValidateLogLevel(t *testing.T) { +func TestRun_ValidateLogLevel(t *testing.T) { cases := []struct { description string - flags map[string]interface{} + flags map[string]flagValue expectError bool }{ { "log level is invalid", - map[string]interface{}{ - LogLevelFlag: "invalid", + map[string]flagValue{ + "log-level": { + Input: "invalid", + }, }, true, }, { "log level is valid uppercase", - map[string]interface{}{ - LogLevelFlag: "DEBUG", + map[string]flagValue{ + "log-level": { + Input: "DEBUG", + }, }, false, }, } for _, testCase := range cases { t.Log("Should validate log level when " + testCase.description) - c := setupWithDefaults(testCase.flags, t) - err := c.Execute() + for k, v := range testCase.flags { + testFlags[k] = v + } + _, err := setup(testFlags, t) if testCase.expectError { Assert(t, err != nil, "should be an error") } else { @@ -298,53 +494,70 @@ func TestExecute_ValidateLogLevel(t *testing.T) { } } -func TestExecute_ValidateCheckoutStrategy(t *testing.T) { - c := setupWithDefaults(map[string]interface{}{ - CheckoutStrategyFlag: "invalid", +func TestRun_ValidateCheckoutStrategy(t *testing.T) { + _, err := setup(map[string]flagValue{ + "checkout-strategy": { + Input: "invalid", + }, }, t) - err := c.Execute() - ErrEquals(t, "invalid checkout strategy: not one of branch or merge", err) + ErrEquals(t, "--checkout-strategy must be one of \"branch\",\"merge\" but got \"invalid\"", err) } -func TestExecute_ValidateSSLConfig(t *testing.T) { - expErr := "--ssl-key-file and --ssl-cert-file are both required for ssl" +func TestRun_ValidateSSLConfig(t *testing.T) { + expErr := "server: both ssl key and certificate are required" cases := []struct { description string - flags map[string]interface{} + flags map[string]flagValue expectError bool }{ { "neither option set", - make(map[string]interface{}), + make(map[string]flagValue), false, }, { "just ssl-key-file set", - map[string]interface{}{ - SSLKeyFileFlag: "file", + map[string]flagValue{ + "ssl-key-file": { + Input: "file", + }, + "ssl-cert-file": { + Input: "", + }, }, true, }, { "just ssl-cert-file set", - map[string]interface{}{ - SSLCertFileFlag: "flag", + map[string]flagValue{ + "ssl-cert-file": { + Input: "flag", + }, + "ssl-key-file": { + Input: "", + }, }, true, }, { "both flags set", - map[string]interface{}{ - SSLCertFileFlag: "cert", - SSLKeyFileFlag: "key", + map[string]flagValue{ + "ssl-cert-file": { + Input: "cert", + }, + "ssl-key-file": { + Input: "key", + }, }, false, }, } for _, testCase := range cases { t.Log("Should validate ssl config when " + testCase.description) - c := setupWithDefaults(testCase.flags, t) - err := c.Execute() + for k, v := range testCase.flags { + testFlags[k] = v + } + _, err := setup(testFlags, t) if testCase.expectError { Assert(t, err != nil, "should be an error") Equals(t, expErr, err.Error()) @@ -354,415 +567,563 @@ func TestExecute_ValidateSSLConfig(t *testing.T) { } } -func TestExecute_ValidateVCSConfig(t *testing.T) { - expErr := "--gh-user/--gh-token or --gh-app-id/--gh-app-key-file or --gh-app-id/--gh-app-key or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set" +func TestRun_ValidateVCSConfig(t *testing.T) { + expErr := "server: credentials for at least one VCS provider should be defined" cases := []struct { description string - flags map[string]interface{} + flags map[string]flagValue expectError bool + customError string }{ { "no config set", - make(map[string]interface{}), + make(map[string]flagValue), true, + "", }, { "just github token set", - map[string]interface{}{ - GHTokenFlag: "token", + map[string]flagValue{ + "gh-token": { + Input: "token", + }, }, true, + "", }, { "just gitlab token set", - map[string]interface{}{ - GitlabTokenFlag: "token", + map[string]flagValue{ + "gitlab-token": { + Input: "token", + }, }, true, + "", }, { "just bitbucket token set", - map[string]interface{}{ - BitbucketTokenFlag: "token", + map[string]flagValue{ + "bitbucket-token": { + Input: "token", + }, }, true, + "", }, { "just azuredevops token set", - map[string]interface{}{ - ADTokenFlag: "token", + map[string]flagValue{ + "azuredevops-token": { + Input: "token", + }, }, true, + "", }, { "just github user set", - map[string]interface{}{ - GHUserFlag: "user", + map[string]flagValue{ + "gh-user": { + Input: "user", + }, }, true, + "server: Github: both user and token should be set", }, { "just github app set", - map[string]interface{}{ - GHAppIDFlag: "1", + map[string]flagValue{ + "gh-app-id": { + Input: "1", + }, }, true, + "server: Github: either app key or app key file should be set together with app ID", }, { "just github app key file set", - map[string]interface{}{ - GHAppKeyFileFlag: "key.pem", + map[string]flagValue{ + "gh-app-key-file": { + Input: "key.pem", + }, }, true, + "", }, { "just github app key set", - map[string]interface{}{ - GHAppKeyFlag: fixtures.GithubPrivateKey, + map[string]flagValue{ + "gh-app-key": { + Input: fixtures.GithubPrivateKey, + }, }, true, + "", }, { "just gitlab user set", - map[string]interface{}{ - GitlabUserFlag: "user", + map[string]flagValue{ + "gitlab-user": { + Input: "user", + }, }, true, + "server: Gitlab: both user and token should be set", }, { "just bitbucket user set", - map[string]interface{}{ - BitbucketUserFlag: "user", + map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, }, true, + "server: Bitbucket: both user and token should be set", }, { "just azuredevops user set", - map[string]interface{}{ - ADUserFlag: "user", + map[string]flagValue{ + "azuredevops-user": { + Input: "user", + }, }, true, + "server: AzureDevops: both user and token should be set", }, { "github user and gitlab token set", - map[string]interface{}{ - GHUserFlag: "user", - GitlabTokenFlag: "token", + map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gitlab-token": { + Input: "token", + }, }, true, + "server: Github: both user and token should be set", }, { "gitlab user and github token set", - map[string]interface{}{ - GitlabUserFlag: "user", - GHTokenFlag: "token", + map[string]flagValue{ + "gitlab-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, }, true, + "server: Github: both user and token should be set", }, { "github user and bitbucket token set", - map[string]interface{}{ - GHUserFlag: "user", - BitbucketTokenFlag: "token", + map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, }, true, + "server: Bitbucket: both user and token should be set", }, { "github user and github token set and should be successful", - map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", + map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, }, false, + "", }, { "github app and key set and should be successful", - map[string]interface{}{ - GHAppIDFlag: "1", - GHAppKeyFlag: fixtures.GithubPrivateKey, + map[string]flagValue{ + "gh-app-id": { + Input: "1", + }, + "gh-app-key": { + Input: fixtures.GithubPrivateKey, + }, }, false, + "", }, { "gitlab user and gitlab token set and should be successful", - map[string]interface{}{ - GitlabUserFlag: "user", - GitlabTokenFlag: "token", + map[string]flagValue{ + "gitlab-user": { + Input: "user", + }, + "gitlab-token": { + Input: "token", + }, }, false, + "", }, { "bitbucket user and bitbucket token set and should be successful", - map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", + map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, }, false, + "", }, { "azuredevops user and azuredevops token set and should be successful", - map[string]interface{}{ - ADUserFlag: "user", - ADTokenFlag: "token", + map[string]flagValue{ + "azuredevops-user": { + Input: "user", + }, + "azuredevops-token": { + Input: "token", + }, }, false, + "", }, { "all set should be successful", - map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - GitlabUserFlag: "user", - GitlabTokenFlag: "token", - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - ADUserFlag: "user", - ADTokenFlag: "token", + map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "gitlab-user": { + Input: "user", + }, + "gitlab-token": { + Input: "token", + }, + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, + "azuredevops-user": { + Input: "user", + }, + "azuredevops-token": { + Input: "token", + }, }, false, + "", }, } for _, testCase := range cases { t.Log("Should validate vcs config when " + testCase.description) - testCase.flags[RepoAllowlistFlag] = "*" + testCase.flags["repo-allowlist"] = flagValue{ + Input: "*", + } - c := setup(testCase.flags, t) - err := c.Execute() + _, err := setup(testCase.flags, t) if testCase.expectError { Assert(t, err != nil, "should be an error") - Equals(t, expErr, err.Error()) + testErr := expErr + if testCase.customError != "" { + testErr = testCase.customError + } + Equals(t, testErr, err.Error()) } else { Ok(t, err) } } } -func TestExecute_ExpandHomeInDataDir(t *testing.T) { +func TestRun_ExpandHomeInDataDir(t *testing.T) { t.Log("If ~ is used as a data-dir path, should expand to absolute home path") - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoAllowlistFlag: "*", - DataDirFlag: "~/this/is/a/path", + c, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, + "data-dir": { + Input: "~/this/is/a/path", + }, }, t) - err := c.Execute() Ok(t, err) - + serverCmd := c.Selected().Target.Interface().(ServerCmd) home, err := homedir.Dir() Ok(t, err) - Equals(t, home+"/this/is/a/path", passedConfig.DataDir) + Equals(t, home+"/this/is/a/path", serverCmd.UserConfig.DataDir) } -func TestExecute_RelativeDataDir(t *testing.T) { +func TestRun_RelativeDataDir(t *testing.T) { t.Log("Should convert relative dir to absolute.") - c := setupWithDefaults(map[string]interface{}{ - DataDirFlag: "../", - }, t) - // Figure out what ../ should be as an absolute path. expectedAbsolutePath, err := filepath.Abs("../") Ok(t, err) + testFlags["data-dir"] = flagValue{ + Input: "../", + } + c, err := setup(testFlags, t) - err = c.Execute() - Ok(t, err) - Equals(t, expectedAbsolutePath, passedConfig.DataDir) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, expectedAbsolutePath, serverCmd.UserConfig.DataDir) } -func TestExecute_GithubUser(t *testing.T) { +func TestRun_GithubUser(t *testing.T) { t.Log("Should remove the @ from the github username if it's passed.") - c := setup(map[string]interface{}{ - GHUserFlag: "@user", - GHTokenFlag: "token", - RepoAllowlistFlag: "*", + c, err := setup(map[string]flagValue{ + "gh-user": { + Input: "@user", + }, + "gh-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) - - Equals(t, "user", passedConfig.GithubUser) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, server.User("user"), serverCmd.UserConfig.GithubSecrets.User) } -func TestExecute_GithubApp(t *testing.T) { +func TestRun_GithubApp(t *testing.T) { t.Log("Should remove the @ from the github username if it's passed.") - c := setup(map[string]interface{}{ - GHAppKeyFlag: fixtures.GithubPrivateKey, - GHAppIDFlag: "1", - RepoAllowlistFlag: "*", + c, err := setup(map[string]flagValue{ + "gh-app-key": { + Input: fixtures.GithubPrivateKey, + }, + "gh-app-id": { + Input: "1", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) - - Equals(t, int64(1), passedConfig.GithubAppID) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, int64(1), serverCmd.UserConfig.GithubSecrets.AppID) } -func TestExecute_GitlabUser(t *testing.T) { +func TestRun_GitlabUser(t *testing.T) { t.Log("Should remove the @ from the gitlab username if it's passed.") - c := setup(map[string]interface{}{ - GitlabUserFlag: "@user", - GitlabTokenFlag: "token", - RepoAllowlistFlag: "*", + c, err := setup(map[string]flagValue{ + "gitlab-user": { + Input: "@user", + }, + "gitlab-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) - - Equals(t, "user", passedConfig.GitlabUser) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, server.User("user"), serverCmd.UserConfig.GitlabSecrets.User) } -func TestExecute_BitbucketUser(t *testing.T) { +func TestRun_BitbucketUser(t *testing.T) { t.Log("Should remove the @ from the bitbucket username if it's passed.") - c := setup(map[string]interface{}{ - BitbucketUserFlag: "@user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", + c, err := setup(map[string]flagValue{ + "bitbucket-user": { + Input: "@user", + }, + "bitbucket-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) - - Equals(t, "user", passedConfig.BitbucketUser) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, server.User("user"), serverCmd.UserConfig.BitbucketSecrets.User) } -func TestExecute_ADUser(t *testing.T) { +func TestRun_ADUser(t *testing.T) { t.Log("Should remove the @ from the azure devops username if it's passed.") - c := setup(map[string]interface{}{ - ADUserFlag: "@user", - ADTokenFlag: "token", - RepoAllowlistFlag: "*", + c, err := setup(map[string]flagValue{ + "azuredevops-user": { + Input: "@user", + }, + "azuredevops-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, }, t) - err := c.Execute() Ok(t, err) - - Equals(t, "user", passedConfig.AzureDevopsUser) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, server.User("user"), serverCmd.UserConfig.AzureDevopsSecrets.User) } // If using bitbucket cloud, webhook secrets are not supported. -func TestExecute_BitbucketCloudWithWebhookSecret(t *testing.T) { - c := setup(map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", - BitbucketWebhookSecretFlag: "my secret", +func TestRun_BitbucketCloudWithWebhookSecret(t *testing.T) { + _, err := setup(map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, + "bitbucket-webhook-secret": { + Input: "my secret", + }, }, t) - err := c.Execute() - ErrEquals(t, "--bitbucket-webhook-secret cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", err) + ErrEquals(t, "server: Bitbucket: webhook secret for Bitbucket Cloud is not supported", err) } // Base URL must have a scheme. -func TestExecute_BitbucketServerBaseURLScheme(t *testing.T) { - c := setup(map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", - BitbucketBaseURLFlag: "mydomain.com", +func TestRun_BitbucketServerBaseURLScheme(t *testing.T) { + _, err := setup(map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, + "bitbucket-base-url": { + Input: "mydomain.com", + }, }, t) - ErrEquals(t, "--bitbucket-base-url must have http:// or https://, got \"mydomain.com\"", c.Execute()) + ErrEquals(t, "--bitbucket-base-url: failed to parse HTTP url: protocol \"\" is not supported", err) - c = setup(map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", - BitbucketBaseURLFlag: "://mydomain.com", + _, err = setup(map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, + "bitbucket-base-url": { + Input: "://mydomain.com", + }, }, t) - ErrEquals(t, "error parsing --bitbucket-webhook-secret flag value \"://mydomain.com\": parse \"://mydomain.com\": missing protocol scheme", c.Execute()) + ErrEquals(t, "--bitbucket-base-url: parse \"://mydomain.com\": missing protocol scheme", err) } // Port should be retained on base url. -func TestExecute_BitbucketServerBaseURLPort(t *testing.T) { - c := setup(map[string]interface{}{ - BitbucketUserFlag: "user", - BitbucketTokenFlag: "token", - RepoAllowlistFlag: "*", - BitbucketBaseURLFlag: "http://mydomain.com:7990", +func TestRun_BitbucketServerBaseURLPort(t *testing.T) { + c, err := setup(map[string]flagValue{ + "bitbucket-user": { + Input: "user", + }, + "bitbucket-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "*", + }, + "bitbucket-base-url": { + Input: "http://mydomain.com:7990", + }, }, t) - Ok(t, c.Execute()) - Equals(t, "http://mydomain.com:7990", passedConfig.BitbucketBaseURL) + Ok(t, err) + serverCmd := c.Selected().Target.Interface().(ServerCmd) + Equals(t, "http://mydomain.com:7990", serverCmd.BitbucketSecrets.BaseURL.String()) } // Can't use both --repo-config and --repo-config-json. -func TestExecute_RepoCfgFlags(t *testing.T) { - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoAllowlistFlag: "github.com", - RepoConfigFlag: "repos.yaml", - RepoConfigJSONFlag: "{}", - }, t) - err := c.Execute() - ErrEquals(t, "cannot use --repo-config and --repo-config-json at the same time", err) -} - -// Can't use both --repo-allowlist and --repo-whitelist -func TestExecute_BothAllowAndWhitelist(t *testing.T) { - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoAllowlistFlag: "github.com", - RepoWhitelistFlag: "github.com", +func TestRun_RepoCfgFlags(t *testing.T) { + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, + "repo-allowlist": { + Input: "github.com", + }, + "repo-config": { + Input: "repos.yaml", + }, + "repo-config-json": { + Input: "{}", + }, }, t) - err := c.Execute() - ErrEquals(t, "both --repo-allowlist and --repo-whitelist cannot be set–use --repo-allowlist", err) + ErrEquals(t, "server: cannot set both path to repo config and repo config json at the same time", err) } // Must set allow or whitelist. -func TestExecute_AllowAndWhitelist(t *testing.T) { - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - }, t) - err := c.Execute() - ErrEquals(t, "--repo-allowlist must be set for security purposes", err) -} - -// Test that we set the corresponding allow list values on the userConfig -// struct if the deprecated whitelist flags are used. -func TestExecute_RepoWhitelistDeprecation(t *testing.T) { - c := setup(map[string]interface{}{ - GHUserFlag: "user", - GHTokenFlag: "token", - RepoWhitelistFlag: "*", +func TestRun_Allowlist(t *testing.T) { + _, err := setup(map[string]flagValue{ + "gh-user": { + Input: "user", + }, + "gh-token": { + Input: "token", + }, }, t) - err := c.Execute() - Ok(t, err) - Equals(t, "*", passedConfig.RepoAllowlist) + ErrEquals(t, "missing flags: --repo-allowlist=REPO-ALLOWLIST,...", err) } -func TestExecute_AutoplanFileList(t *testing.T) { +func TestRun_AutoplanFileList(t *testing.T) { cases := []struct { description string - flags map[string]interface{} + flags map[string]flagValue expectErr string }{ - { - "default value", - map[string]interface{}{ - AutoplanFileListFlag: DefaultAutoplanFileList, - }, - "", - }, { "valid value", - map[string]interface{}{ - AutoplanFileListFlag: "**/*.tf", + map[string]flagValue{ + "autoplan-file-list": { + Input: "**/*.tf", + }, }, "", }, { "invalid exclusion pattern", - map[string]interface{}{ - AutoplanFileListFlag: "**/*.yml,!", + map[string]flagValue{ + "autoplan-file-list": { + Input: "**/*.yml,!", + }, }, - "invalid pattern in --autoplan-file-list, **/*.yml,!: illegal exclusion pattern: \"!\"", + "--autoplan-file-list: illegal exclusion pattern: \"!\"", }, { "invalid pattern", - map[string]interface{}{ - AutoplanFileListFlag: "[^]", + map[string]flagValue{ + "autoplan-file-list": { + Input: "[^]", + }, }, - "invalid pattern in --autoplan-file-list, [^]: syntax error in pattern", + "--autoplan-file-list: syntax error in pattern", }, } for _, testCase := range cases { t.Log("Should validate autoplan file list when " + testCase.description) - c := setupWithDefaults(testCase.flags, t) - err := c.Execute() + for k, v := range testCase.flags { + testFlags[k] = v + } + _, err := setup(testFlags, t) if testCase.expectErr != "" { ErrEquals(t, testCase.expectErr, err) } else { @@ -771,34 +1132,27 @@ func TestExecute_AutoplanFileList(t *testing.T) { } } -func setup(flags map[string]interface{}, _ *testing.T) *cobra.Command { - vipr := viper.New() - for k, v := range flags { - vipr.Set(k, v) - } - c := &ServerCmd{ - ServerCreator: &ServerCreatorMock{}, - Viper: vipr, - SilenceOutput: true, - } - return c.Init() -} - -func setupWithDefaults(flags map[string]interface{}, _ *testing.T) *cobra.Command { - vipr := viper.New() - flags[GHUserFlag] = "user" - flags[GHTokenFlag] = "token" - flags[RepoAllowlistFlag] = "*" +func setup(args map[string]flagValue, _ *testing.T) (*kong.Context, error) { + parser, _ := kong.New( + &CLI, + FlagsVars, + kong.DefaultEnvars("ATLANTIS"), + ) - for k, v := range flags { - vipr.Set(k, v) - } - c := &ServerCmd{ - ServerCreator: &ServerCreatorMock{}, - Viper: vipr, - SilenceOutput: true, + cmdline := []string{"server"} + for k, v := range args { + val := "" + switch v.Input.(type) { + case bool: + val = fmt.Sprintf("%t", v.Input.(bool)) + case string: + val = v.Input.(string) + case int: + val = fmt.Sprintf("%d", v.Input.(int)) + } + cmdline = append(cmdline, fmt.Sprintf("--%s=%s", k, val)) } - return c.Init() + return parser.Parse(cmdline) } func tempFile(t *testing.T, contents string) string { @@ -811,15 +1165,13 @@ func tempFile(t *testing.T, contents string) string { return newName } -func configVal(t *testing.T, u server.UserConfig, tag string) interface{} { +func configVal(t *testing.T, ctx *kong.Context, tag string) interface{} { t.Helper() - v := reflect.ValueOf(u) - typeOfS := v.Type() - for i := 0; i < v.NumField(); i++ { - if typeOfS.Field(i).Tag.Get("mapstructure") == tag { - return v.Field(i).Interface() + for _, flag := range ctx.Flags() { + if flag.Name == tag { + return ctx.FlagValue(flag) } } t.Fatalf("no field with tag %q found", tag) - return nil + return "" } diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 7b9da7797..000000000 --- a/cmd/version.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 HootSuite Media Inc. -// -// 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. -// Modified hereafter by contributors to runatlantis/atlantis. - -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// VersionCmd prints the current version. -type VersionCmd struct { - AtlantisVersion string -} - -// Init returns the runnable cobra command. -func (v *VersionCmd) Init() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Print the current Atlantis version", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("atlantis %s\n", v.AtlantisVersion) - }, - } -} diff --git a/go.mod b/go.mod index 53772d8f9..eab834143 100644 --- a/go.mod +++ b/go.mod @@ -17,13 +17,11 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bradleyfalzon/ghinstallation v1.1.1 - github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docker/docker v0.0.0-20180620051407-e2593239d949 github.com/elazarl/go-bindata-assetfs v1.0.1 - github.com/fatih/color v1.7.0 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/fsnotify/fsnotify v1.4.10-0.20200417215612-7f4cf4dd2b52 // indirect github.com/go-ozzo/ozzo-validation v0.0.0-20170913164239-85dcd8368eba @@ -51,23 +49,19 @@ require ( github.com/hashicorp/terraform-config-inspect v0.0.0-20200806211835-c481b8bfa41e github.com/huandu/xstrings v1.3.1 // indirect github.com/imdario/mergo v0.3.11 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jstemmer/go-junit-report v0.9.1 // indirect github.com/klauspost/compress v1.11.2 // indirect github.com/leodido/go-urn v1.2.0 // indirect github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect - github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mcdafydd/go-azuredevops v0.12.0 github.com/microcosm-cc/bluemonday v1.0.15 - github.com/mitchellh/colorstring v0.0.0-20150917214807-8631ce90f286 github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb github.com/nlopes/slack v0.4.0 @@ -83,14 +77,9 @@ require ( github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f // indirect - github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v0.0.0-20170905172051-b78744579491 - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.8.1 - github.com/subosito/gotenv v1.2.0 // indirect github.com/thomaspoignant/go-feature-flag v0.18.4 github.com/ulikunitz/xz v0.5.8 // indirect github.com/urfave/cli v1.22.5 @@ -106,7 +95,7 @@ require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.1.0 // indirect - golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.4.0 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect @@ -118,9 +107,8 @@ require ( google.golang.org/protobuf v1.28.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v9 v9.31.0 - gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible // indirect ) @@ -135,6 +123,8 @@ require ( require go.temporal.io/sdk v1.15.0 require ( + github.com/alecthomas/kong v0.7.1 + github.com/alecthomas/kong-yaml v0.1.1 github.com/aws/aws-sdk-go-v2 v1.13.0 github.com/aws/aws-sdk-go-v2/config v1.13.1 github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 @@ -180,7 +170,6 @@ require ( github.com/pborman/uuid v1.2.1 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.0 // indirect - go.temporal.io/api v1.8.0 // indirect + go.temporal.io/api v1.8.0 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 - golang.org/x/term v0.1.0 ) diff --git a/go.sum b/go.sum index 6198f4769..11262bd00 100644 --- a/go.sum +++ b/go.sum @@ -26,7 +26,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -63,6 +62,13 @@ github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= +github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/kong-yaml v0.1.1 h1:FOzG9AfTze44xhbuC4AJbDsSSgviRstBvKEGO4PaeTQ= +github.com/alecthomas/kong-yaml v0.1.1/go.mod h1:RcZd2uNxAnzxASa+z6rzD+ldFG60hdR2nCzlHOpPCBE= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -77,9 +83,6 @@ github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbj github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= @@ -119,16 +122,12 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I= github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug= github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 h1:5+NghM1Zred9Z078QEZtm28G/kfDfZN/92gkDlLwGVA= github.com/bradleyfalzon/ghinstallation/v2 v2.1.0/go.mod h1:Xg3xPRN5Mcq6GDqeUVhFbjEWMb4JHCyWEeeBGEYQoTU= -github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 h1:osZyZB7J4kE1tKLeaUjV6+uZVBfS835T0I/RxmwWw1w= -github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/cactus/go-statsd-client/statsd v0.0.0-20200623234511-94959e3146b2 h1:GgJnJEJYymy/lx+1zXOO2TvGPRQJJ9vz4onxnA9gF3k= github.com/cactus/go-statsd-client/statsd v0.0.0-20200623234511-94959e3146b2/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= @@ -145,8 +144,6 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -166,7 +163,6 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= @@ -289,8 +285,6 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -305,8 +299,6 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -315,22 +307,14 @@ github.com/hashicorp/go-getter v1.5.7 h1:HBLsom8eGHLxj78ta+/MVSyct8KWG4B4z6lhBA4 github.com/hashicorp/go-getter v1.5.7/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -341,14 +325,11 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o= github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/terraform-config-inspect v0.0.0-20200806211835-c481b8bfa41e h1:wIsEsIITggCC4FTO9PisDjy561UU7OPL6uTu7tnkHH8= github.com/hashicorp/terraform-config-inspect v0.0.0-20200806211835-c481b8bfa41e/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs= github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -356,8 +337,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -371,8 +350,6 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -396,12 +373,9 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 h1:MNApn+Z+fIT4NPZopPfCc1obT6aY3SVM6DOctz1A9ZU= github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -411,10 +385,6 @@ github.com/mcdafydd/go-azuredevops v0.12.0 h1:CmG9uheFF6M3WnSykVNVLxR7zXrtg4p3pE github.com/mcdafydd/go-azuredevops v0.12.0/go.mod h1:B4UDyn7WEj1/97f45j3VnzEfkWKe05+/dCcAPdOET4A= github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY= github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/colorstring v0.0.0-20150917214807-8631ce90f286 h1:KHyL+3mQOF9sPfs26lsefckcFNDcIZtiACQiECzIUkw= -github.com/mitchellh/colorstring v0.0.0-20150917214807-8631ce90f286/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -426,12 +396,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -462,7 +426,6 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/palantir/go-githubapp v0.13.1 h1:nxFcMAdPIBw1/xsEKCkNcqY2fGZ+LtOK/oxHT4rL5cY= github.com/palantir/go-githubapp v0.13.1/go.mod h1:W6qVR/EY4l94mRF/J4Qwop8xTQAf0iUXVmX1ipDJnQM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= @@ -475,10 +438,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -510,9 +471,7 @@ github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -530,25 +489,13 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f h1:qqqIhBDFUBrbMezIyJkKWIpf+E5CdObleGMjW1s19Hg= github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.0-20170905172051-b78744579491 h1:XOya2OGpG7Q4gS4MYHRoFSTlBGnZD40X+Kw2ikFQFXE= -github.com/spf13/cobra v0.0.0-20170905172051-b78744579491/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -565,8 +512,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/thomaspoignant/go-feature-flag v0.18.4 h1:LJ5dns5HRMzXATggfXKHanrGriN0DfQ5cm2N7U9g/0A= github.com/thomaspoignant/go-feature-flag v0.18.4/go.mod h1:S/z9nx718SSumYgGL0LK71QRl3lQvFLAaQlPzexgMus= github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= @@ -599,9 +544,6 @@ github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uU github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -633,17 +575,14 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -693,9 +632,7 @@ golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -765,10 +702,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -831,8 +766,6 @@ golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -854,7 +787,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -867,7 +799,6 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -902,7 +833,6 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.2.1-0.20221101170700-b5bc717366b2 h1:KBm+UwBaO/tdQ35tfGvxH1FUCiXRg4MoTzkznsdeab8= @@ -985,7 +915,6 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= @@ -1019,8 +948,6 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/kothar/go-backblaze.v0 v0.0.0-20190520213052-702d4e7eb465/go.mod h1:zJ2QpyDCYo1KvLXlmdnFlQAyF/Qfth0fB8239Qg7BIE= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/main.go b/main.go index c9ff1fd1c..e0258496e 100644 --- a/main.go +++ b/main.go @@ -17,22 +17,23 @@ package main import ( + "github.com/alecthomas/kong" "github.com/runatlantis/atlantis/cmd" - "github.com/spf13/viper" ) const atlantisVersion = "0.17.3" func main() { - v := viper.New() - - // We're creating commands manually here rather than using init() functions - // (as recommended by cobra) because it makes testing easier. - server := cmd.NewServerCmd(v, atlantisVersion) - version := &cmd.VersionCmd{AtlantisVersion: atlantisVersion} - - cmd.RootCmd.AddCommand(server.Init()) - cmd.RootCmd.AddCommand(version.Init()) - - cmd.Execute() + ctx := kong.Parse( + &cmd.CLI, + cmd.FlagsVars, + kong.DefaultEnvars("ATLANTIS"), + kong.Bind(cmd.Context{ + Version: atlantisVersion, + }), + ) + err := ctx.Run(&cmd.Context{ + Version: atlantisVersion, + }) + ctx.FatalIfErrorf(err) } diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index bebb427f6..2578c6fe3 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -4,6 +4,17 @@ import ( "bytes" "context" "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + + "github.com/docker/docker/pkg/fileutils" "github.com/google/go-github/v45/github" "github.com/hashicorp/go-getter" "github.com/hashicorp/go-version" @@ -22,15 +33,6 @@ import ( github_converter "github.com/runatlantis/atlantis/server/vcs/provider/github/converter" "github.com/runatlantis/atlantis/server/vcs/provider/github/request" ffclient "github.com/thomaspoignant/go-feature-flag" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "sync" - "testing" "github.com/runatlantis/atlantis/server/core/runtime/policy" "github.com/runatlantis/atlantis/server/core/terraform" @@ -716,7 +718,7 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig t.Cleanup(featureAllocator.Close) - terraformClient, err := terraform.NewE2ETestClient(binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", downloader, false, projectCmdOutputHandler) + terraformClient, err := terraform.NewE2ETestClient(binDir, cacheDir, "", "", "", "https://releases.hashicorp.com", downloader, false, projectCmdOutputHandler) Ok(t, err) // Set real dependencies here. @@ -791,6 +793,8 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig projectContextBuilder = projectContextBuilder.EnablePolicyChecks(commentParser) } + repoAllowlist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + projectCommandBuilder := events.NewProjectCommandBuilder( projectContextBuilder, parser, @@ -801,7 +805,7 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig globalCfg, &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + repoAllowlist, ctxLogger, events.InfiniteProjectsPerPR, ) @@ -996,7 +1000,7 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig PolicyCommandRunner: prrPolicyCommandRunner, } - repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") + repoAllowlistChecker, err := events.NewRepoAllowlistChecker([]string{"*"}) Ok(t, err) autoplanner := &handlers.Autoplanner{ @@ -1041,8 +1045,8 @@ func setupE2E(t *testing.T, repoFixtureDir string, userConfig *server.UserConfig ) repoConverter := github_converter.RepoConverter{ - GithubUser: userConfig.GithubUser, - GithubToken: userConfig.GithubToken, + GithubUser: string(userConfig.GithubSecrets.User), + GithubToken: userConfig.GithubSecrets.Token, } pullConverter := github_converter.PullConverter{ diff --git a/server/controllers/events/events_controller_test.go b/server/controllers/events/events_controller_test.go index 0c473e484..326289698 100644 --- a/server/controllers/events/events_controller_test.go +++ b/server/controllers/events/events_controller_test.go @@ -208,7 +208,7 @@ func TestPost_BBServerPullClosed(t *testing.T) { for _, c := range cases { t.Run(c.header, func(t *testing.T) { RegisterMockTestingT(t) - allowlist, err := events.NewRepoAllowlistChecker("*") + allowlist, err := events.NewRepoAllowlistChecker([]string{"*"}) Ok(t, err) ctxLogger := logging.NewNoopCtxLogger(t) scope, _, _ := metrics.NewLoggingScope(ctxLogger, "null") @@ -301,7 +301,7 @@ func setup(t *testing.T) (events_controllers.VCSEventsController, *mocks.MockGit cp := emocks.NewMockCommentParsing() c := emocks.NewMockPullCleaner() vcsmock := vcsmocks.NewMockClient() - repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") + repoAllowlistChecker, err := events.NewRepoAllowlistChecker([]string{"*"}) Ok(t, err) ctxLogger := logging.NewNoopCtxLogger(t) scope, _, _ := metrics.NewLoggingScope(ctxLogger, "null") diff --git a/server/controllers/events/handlers/comment_test.go b/server/controllers/events/handlers/comment_test.go index 00c51609f..981665a21 100644 --- a/server/controllers/events/handlers/comment_test.go +++ b/server/controllers/events/handlers/comment_test.go @@ -62,7 +62,7 @@ func TestCommentHandler(t *testing.T) { event := event_types.Comment{} - repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") + repoAllowlistChecker, err := events.NewRepoAllowlistChecker([]string{"*"}) assert.NoError(t, err) t.Run("success", func(t *testing.T) { diff --git a/server/controllers/events/handlers/pull_request_test.go b/server/controllers/events/handlers/pull_request_test.go index cf81acc1d..627caebdb 100644 --- a/server/controllers/events/handlers/pull_request_test.go +++ b/server/controllers/events/handlers/pull_request_test.go @@ -42,7 +42,7 @@ func TestPREventHandler(t *testing.T) { Pull: models.PullRequest{Num: 1}, } - repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") + repoAllowlistChecker, err := events.NewRepoAllowlistChecker([]string{"*"}) assert.NoError(t, err) openPREventHandler := &assertingPRHandler{expectedEvent: event, expectedRequest: request, t: t} diff --git a/server/core/terraform/terraform_client.go b/server/core/terraform/terraform_client.go index e43213391..9bfb7341b 100644 --- a/server/core/terraform/terraform_client.go +++ b/server/core/terraform/terraform_client.go @@ -87,14 +87,13 @@ func NewClientWithVersionCache( binDir string, cacheDir string, defaultVersionStr string, - defaultVersionFlagName string, tfDownloadURL string, tfDownloader Downloader, usePluginCache bool, projectCmdOutputHandler jobs.ProjectCommandOutputHandler, versionCache cache.ExecutionVersionCache, ) (*DefaultClient, error) { - version, err := getDefaultVersion(defaultVersionStr, defaultVersionFlagName) + version, err := getDefaultVersion(defaultVersionStr) if err != nil { return nil, errors.Wrapf(err, "getting default version") @@ -138,7 +137,6 @@ func NewE2ETestClient( tfeToken string, tfeHostname string, defaultVersionStr string, - defaultVersionFlagName string, tfDownloadURL string, tfDownloader Downloader, usePluginCache bool, @@ -149,7 +147,6 @@ func NewE2ETestClient( binDir, cacheDir, defaultVersionStr, - defaultVersionFlagName, tfDownloadURL, tfDownloader, usePluginCache, @@ -162,7 +159,6 @@ func NewClient( binDir string, cacheDir string, defaultVersionStr string, - defaultVersionFlagName string, tfDownloadURL string, tfDownloader Downloader, usePluginCache bool, @@ -182,7 +178,6 @@ func NewClient( binDir, cacheDir, defaultVersionStr, - defaultVersionFlagName, tfDownloadURL, tfDownloader, usePluginCache, @@ -294,7 +289,7 @@ func isAsyncEligibleCommand(cmd string) bool { return false } -func getDefaultVersion(overrideVersion string, versionFlagName string) (*version.Version, error) { +func getDefaultVersion(overrideVersion string) (*version.Version, error) { if overrideVersion != "" { v, err := version.NewVersion(overrideVersion) if err != nil { @@ -310,7 +305,7 @@ func getDefaultVersion(overrideVersion string, versionFlagName string) (*version // and if thats the case we won't be redownloading the version of this binary to our cache localPath, err := exec.LookPath("terraform") if err != nil { - return nil, fmt.Errorf("terraform not found in $PATH. Set --%s or download terraform from https://www.terraform.io/downloads.html", versionFlagName) + return nil, fmt.Errorf("terraform not found in $PATH. Set default TF version flag or download terraform from https://www.terraform.io/downloads.html") } return getVersion(localPath) diff --git a/server/core/terraform/terraform_client_test.go b/server/core/terraform/terraform_client_test.go index ae6e3661c..a37b151fc 100644 --- a/server/core/terraform/terraform_client_test.go +++ b/server/core/terraform/terraform_client_test.go @@ -20,7 +20,6 @@ import ( "path/filepath" "testing" - "github.com/runatlantis/atlantis/cmd" "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" @@ -39,8 +38,8 @@ func TestNewClient_NoTF(t *testing.T) { // Set PATH to only include our empty directory. defer tempSetEnv(t, "PATH", tmp)() - _, err := terraform.NewClient(binDir, cacheDir, "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler) - ErrEquals(t, "getting default version: terraform not found in $PATH. Set --default-tf-version or download terraform from https://www.terraform.io/downloads.html", err) + _, err := terraform.NewClient(binDir, cacheDir, "", "", nil, true, projectCmdOutputHandler) + ErrEquals(t, "getting default version: terraform not found in $PATH. Set default TF version flag or download terraform from https://www.terraform.io/downloads.html", err) } // Test that if the default-tf flag is set and that binary is in our PATH @@ -65,7 +64,7 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) { Ok(t, err) defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))() - c, err := terraform.NewClient(binDir, cacheDir, "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler) + c, err := terraform.NewClient(binDir, cacheDir, "0.11.10", "", nil, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) @@ -97,7 +96,7 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) { Ok(t, err) defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))() - c, err := terraform.NewClient(binDir, cacheDir, "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler) + c, err := terraform.NewClient(binDir, cacheDir, "0.11.10", "", nil, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) @@ -114,7 +113,7 @@ func TestNewClient_BadVersion(t *testing.T) { projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() defer cleanup() - _, err := terraform.NewClient(binDir, cacheDir, "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler) + _, err := terraform.NewClient(binDir, cacheDir, "malformed", "", nil, true, projectCmdOutputHandler) ErrEquals(t, "getting default version: parsing version malformed: Malformed version: malformed", err) } diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index b94af554a..a6604d5b6 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/docker/docker/pkg/fileutils" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/logging" @@ -39,7 +40,7 @@ func NewProjectCommandBuilder( globalCfg valid.GlobalCfg, pendingPlanFinder *DefaultPendingPlanFinder, EnableRegExpCmd bool, - AutoplanFileList string, + AutoplanFileList *fileutils.PatternMatcher, logger logging.Logger, limit int, ) ProjectCommandBuilder { @@ -114,7 +115,7 @@ type DefaultProjectCommandBuilder struct { PendingPlanFinder *DefaultPendingPlanFinder ProjectCommandContextBuilder ProjectCommandContextBuilder EnableRegExpCmd bool - AutoplanFileList string + AutoplanFileList *fileutils.PatternMatcher EnableDiffMarkdownFormat bool } diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index 707e993d2..02d86cbd9 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/docker/docker/pkg/fileutils" version "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/core/config" @@ -580,6 +581,8 @@ projects: Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := &DefaultProjectCommandBuilder{ ParserValidator: &config.ParserValidator{}, ProjectFinder: &DefaultProjectFinder{}, @@ -591,7 +594,7 @@ projects: ProjectCommandContextBuilder: &projectCommandContextBuilder{ CommentBuilder: &CommentParser{}, }, - AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + AutoplanFileList: autoplanFilelist, EnableRegExpCmd: false, } @@ -749,6 +752,8 @@ workflows: Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := &DefaultProjectCommandBuilder{ ParserValidator: &config.ParserValidator{}, ProjectFinder: &DefaultProjectFinder{}, @@ -760,7 +765,7 @@ workflows: ProjectCommandContextBuilder: &projectCommandContextBuilder{ CommentBuilder: &CommentParser{}, }, - AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + AutoplanFileList: autoplanFilelist, EnableRegExpCmd: false, } @@ -950,6 +955,8 @@ projects: Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := &DefaultProjectCommandBuilder{ ParserValidator: &config.ParserValidator{}, ProjectFinder: &DefaultProjectFinder{}, @@ -961,7 +968,7 @@ projects: ProjectCommandContextBuilder: &projectCommandContextBuilder{ CommentBuilder: &CommentParser{}, }, - AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + AutoplanFileList: autoplanFilelist, EnableRegExpCmd: true, } @@ -1168,6 +1175,7 @@ workflows: contextBuilder, &CommentParser{}, } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) builder := &DefaultProjectCommandBuilder{ ParserValidator: &config.ParserValidator{}, ProjectFinder: &DefaultProjectFinder{}, @@ -1177,7 +1185,7 @@ workflows: GlobalCfg: globalCfg, PendingPlanFinder: &DefaultPendingPlanFinder{}, ProjectCommandContextBuilder: contextBuilder, - AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + AutoplanFileList: autoplanFilelist, } cmd := command.PolicyCheck diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 9982c9390..84a15b443 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -3,12 +3,14 @@ package events_test import ( "context" "fmt" - "github.com/stretchr/testify/assert" "os" "path/filepath" "strings" "testing" + "github.com/docker/docker/pkg/fileutils" + "github.com/stretchr/testify/assert" + . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/core/config/valid" @@ -143,6 +145,8 @@ projects: Ok(t, err) } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -153,7 +157,7 @@ projects: valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -406,6 +410,13 @@ projects: globalCfg := valid.NewGlobalCfg("somedir") globalCfg.Repos[0].AllowedOverrides = []string{"apply_requirements"} + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{ + "**/*.tf", + "**/*.tfvars", + "**/*.tfvars.json", + "**/terragrunt.hcl", + }) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -416,7 +427,7 @@ projects: globalCfg, &events.DefaultPendingPlanFinder{}, true, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -553,6 +564,8 @@ projects: Ok(t, err) } + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -563,7 +576,7 @@ projects: valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -636,6 +649,8 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -646,7 +661,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -713,6 +728,8 @@ projects: logger := logging.NewNoopCtxLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -723,7 +740,7 @@ projects: valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -783,6 +800,8 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { vcsClient := vcsmocks.NewMockClient() When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil) + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -793,7 +812,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -957,6 +976,8 @@ projects: matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, nil) + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -967,7 +988,7 @@ projects: valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -1019,6 +1040,8 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman WrapProjectContext(events.NewProjectCommandContextBuilder(commentParser)). EnablePolicyChecks(commentParser) + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( contextBuilder, &config.ParserValidator{}, @@ -1029,7 +1052,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman globalCfg, &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) @@ -1093,6 +1116,8 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { logger := logging.NewNoopCtxLogger(t) scope := tally.NewTestScope("test", nil) + autoplanFilelist, _ := fileutils.NewPatternMatcher([]string{"**/*.tf", "**/*.tfvars", "**/*.tfvars.json", "**/terragrunt.hcl"}) + builder := events.NewProjectCommandBuilder( events.NewProjectCommandContextBuilder(&events.CommentParser{}), &config.ParserValidator{}, @@ -1103,7 +1128,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { valid.NewGlobalCfg("somedir"), &events.DefaultPendingPlanFinder{}, false, - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl", + autoplanFilelist, logger, events.InfiniteProjectsPerPR, ) diff --git a/server/events/project_finder.go b/server/events/project_finder.go index b1603ce7d..d9f49a813 100644 --- a/server/events/project_finder.go +++ b/server/events/project_finder.go @@ -35,7 +35,7 @@ type ProjectFinder interface { // DetermineProjects returns the list of projects that were modified based on // the modifiedFiles. The list will be de-duplicated. // absRepoDir is the path to the cloned repo on disk. - DetermineProjects(requestCtx context.Context, log logging.Logger, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList string) []models.Project + DetermineProjects(requestCtx context.Context, log logging.Logger, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList *fileutils.PatternMatcher) []models.Project // DetermineProjectsViaConfig returns the list of projects that were modified // based on modifiedFiles and the repo's config. // absRepoDir is the path to the cloned repo on disk. @@ -49,7 +49,7 @@ var ignoredFilenameFragments = []string{"terraform.tfstate", "terraform.tfstate. type DefaultProjectFinder struct{} // See ProjectFinder.DetermineProjects. -func (p *DefaultProjectFinder) DetermineProjects(requestCtx context.Context, log logging.Logger, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList string) []models.Project { +func (p *DefaultProjectFinder) DetermineProjects(requestCtx context.Context, log logging.Logger, modifiedFiles []string, repoFullName string, absRepoDir string, autoplanFileList *fileutils.PatternMatcher) []models.Project { var projects []models.Project modifiedTerraformFiles := p.filterToFileList(modifiedFiles, autoplanFileList) @@ -142,11 +142,8 @@ func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log logging.Logger, mo } // filterToFileList filters out files not included in the file list -func (p *DefaultProjectFinder) filterToFileList(files []string, fileList string) []string { +func (p *DefaultProjectFinder) filterToFileList(files []string, patternMatcher *fileutils.PatternMatcher) []string { var filtered []string - patterns := strings.Split(fileList, ",") - // Ignore pattern matcher error here as it was checked for errors in server validation - patternMatcher, _ := fileutils.NewPatternMatcher(patterns) for _, fileName := range files { if p.shouldIgnore(fileName) { diff --git a/server/events/project_finder_test.go b/server/events/project_finder_test.go index a23dafb95..12687d20a 100644 --- a/server/events/project_finder_test.go +++ b/server/events/project_finder_test.go @@ -17,8 +17,10 @@ import ( "context" "os" "path/filepath" + "strings" "testing" + "github.com/docker/docker/pkg/fileutils" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/logging" @@ -252,7 +254,8 @@ func TestDetermineProjects(t *testing.T) { } for _, c := range cases { t.Run(c.description, func(t *testing.T) { - projects := m.DetermineProjects(context.TODO(), noopLogger, c.files, modifiedRepo, c.repoDir, c.autoplanFileList) + autoplanFilelist, _ := fileutils.NewPatternMatcher(strings.Split(c.autoplanFileList, ",")) + projects := m.DetermineProjects(context.TODO(), noopLogger, c.files, modifiedRepo, c.repoDir, autoplanFilelist) // Extract the paths from the projects. We use a slice here instead of a // map so we can test whether there are duplicates returned. diff --git a/server/events/repo_allowlist_checker.go b/server/events/repo_allowlist_checker.go index 5acc4e705..9a2d88c61 100644 --- a/server/events/repo_allowlist_checker.go +++ b/server/events/repo_allowlist_checker.go @@ -29,13 +29,7 @@ type RepoAllowlistChecker struct { // NewRepoAllowlistChecker constructs a new checker and validates that the // allowlist isn't malformed. -func NewRepoAllowlistChecker(allowlist string) (*RepoAllowlistChecker, error) { - rules := strings.Split(allowlist, ",") - for _, rule := range rules { - if strings.Contains(rule, "://") { - return nil, fmt.Errorf("allowlist %q contained ://", rule) - } - } +func NewRepoAllowlistChecker(rules []string) (*RepoAllowlistChecker, error) { return &RepoAllowlistChecker{ rules: rules, }, nil diff --git a/server/events/repo_allowlist_checker_test.go b/server/events/repo_allowlist_checker_test.go index 79ff3d461..8a4c7d4cb 100644 --- a/server/events/repo_allowlist_checker_test.go +++ b/server/events/repo_allowlist_checker_test.go @@ -14,6 +14,7 @@ package events_test import ( + "strings" "testing" "github.com/runatlantis/atlantis/server/events" @@ -179,33 +180,9 @@ func TestRepoAllowlistChecker_IsAllowlisted(t *testing.T) { for _, c := range cases { t.Run(c.Description, func(t *testing.T) { - w, err := events.NewRepoAllowlistChecker(c.Allowlist) + w, err := events.NewRepoAllowlistChecker(strings.Split(c.Allowlist, ",")) Ok(t, err) Equals(t, c.Exp, w.IsAllowlisted(c.RepoFullName, c.Hostname)) }) } } - -// If the allowlist contains a schema then we should get an error. -func TestRepoAllowlistChecker_ContainsSchema(t *testing.T) { - cases := []struct { - allowlist string - expErr string - }{ - { - "://", - `allowlist "://" contained ://`, - }, - { - "valid/*,https://bitbucket.org/*", - `allowlist "https://bitbucket.org/*" contained ://`, - }, - } - - for _, c := range cases { - t.Run(c.allowlist, func(t *testing.T) { - _, err := events.NewRepoAllowlistChecker(c.allowlist) - ErrEquals(t, c.expErr, err) - }) - } -} diff --git a/server/logging/logger.go b/server/logging/logger.go index d1161a45f..850060e7c 100644 --- a/server/logging/logger.go +++ b/server/logging/logger.go @@ -18,9 +18,13 @@ package logging import ( "context" + "fmt" "io" + "reflect" + "strings" "testing" + "github.com/alecthomas/kong" "github.com/pkg/errors" ctxInternal "github.com/runatlantis/atlantis/server/neptune/context" "go.uber.org/zap" @@ -193,6 +197,27 @@ type LogLevel struct { shortStr string } +func (l *LogLevel) Decode(ctx *kong.DecodeContext) error { + var rawLevel string + err := ctx.Scan.PopValueInto("string", &rawLevel) + if err != nil { + return err + } + switch strings.ToLower(rawLevel) { + case "debug": + ctx.Value.Target.Set(reflect.ValueOf(Debug)) + case "info": + ctx.Value.Target.Set(reflect.ValueOf(Info)) + case "warn": + ctx.Value.Target.Set(reflect.ValueOf(Warn)) + case "error": + ctx.Value.Target.Set(reflect.ValueOf(Error)) + default: + return fmt.Errorf("log level %q is not supported", rawLevel) + } + return nil +} + var ( Debug = LogLevel{ zLevel: zapcore.DebugLevel, diff --git a/server/neptune/gateway/server.go b/server/neptune/gateway/server.go index 113d45c18..75e7943be 100644 --- a/server/neptune/gateway/server.go +++ b/server/neptune/gateway/server.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/docker/docker/pkg/fileutils" "github.com/palantir/go-githubapp/githubapp" "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/controllers" @@ -52,9 +53,9 @@ import ( // TODO: let's make this struct nicer using actual OOP instead of just a god type struct type Config struct { DataDir string - AutoplanFileList string + AutoplanFileList *fileutils.PatternMatcher AppCfg githubapp.Config - RepoAllowList string + RepoAllowlist []string MaxProjectsPerPR int FFOwner string FFRepo string @@ -97,7 +98,7 @@ func NewServer(config Config) (*Server, error) { return nil, err } - repoAllowlist, err := events.NewRepoAllowlistChecker(config.RepoAllowList) + repoAllowlist, err := events.NewRepoAllowlistChecker(config.RepoAllowlist) if err != nil { return nil, err } diff --git a/server/router_test.go b/server/router_test.go index 4546e695e..ec993ad94 100644 --- a/server/router_test.go +++ b/server/router_test.go @@ -3,6 +3,7 @@ package server_test import ( "fmt" "net/http" + "net/url" "testing" "github.com/google/uuid" @@ -16,31 +17,44 @@ import ( func TestRouter_GenerateLockURL(t *testing.T) { cases := []struct { - AtlantisURL string + AtlantisURL *url.URL ExpURL string }{ { - "http://localhost:4141", + &url.URL{ + Scheme: "http", + Host: "localhost:4141", + }, "http://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", }, { - "https://localhost:4141", + &url.URL{ + Scheme: "https", + Host: "localhost:4141", + }, "https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", }, { - "https://localhost:4141/", + &url.URL{ + Scheme: "https", + Host: "localhost:4141", + }, "https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", }, { - "https://example.com/basepath", - "https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", - }, - { - "https://example.com/basepath/", + &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/basepath", + }, "https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", }, { - "https://example.com/path/1/", + &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "/path/1", + }, "https://example.com/path/1/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault", }, } @@ -51,12 +65,9 @@ func TestRouter_GenerateLockURL(t *testing.T) { underlyingRouter.HandleFunc("/lock", func(_ http.ResponseWriter, _ *http.Request) {}).Methods(http.MethodGet).Queries(queryParam, "{id}").Name(routeName) for _, c := range cases { - t.Run(c.AtlantisURL, func(t *testing.T) { - atlantisURL, err := server.ParseAtlantisURL(c.AtlantisURL) - Ok(t, err) - + t.Run(c.AtlantisURL.String(), func(t *testing.T) { router := &server.Router{ - AtlantisURL: atlantisURL, + AtlantisURL: c.AtlantisURL, LockViewRouteIDQueryParam: queryParam, LockViewRouteName: routeName, Underlying: underlyingRouter, @@ -67,14 +78,14 @@ func TestRouter_GenerateLockURL(t *testing.T) { } func setupJobsRouter(t *testing.T) *server.Router { - atlantisURL, err := server.ParseAtlantisURL("http://localhost:4141") - Ok(t, err) - underlyingRouter := mux.NewRouter() underlyingRouter.HandleFunc("/jobs/{job-id}", func(_ http.ResponseWriter, _ *http.Request) {}).Methods(http.MethodGet).Name("project-jobs-detail") return &server.Router{ - AtlantisURL: atlantisURL, + AtlantisURL: &url.URL{ + Scheme: "http", + Host: "localhost:4141", + }, Underlying: underlyingRouter, ProjectJobsViewRouteName: "project-jobs-detail", } diff --git a/server/server.go b/server/server.go index 8a0cbb3fd..0774ec49f 100644 --- a/server/server.go +++ b/server/server.go @@ -27,7 +27,6 @@ import ( "os/signal" "path/filepath" "sort" - "strings" "syscall" "time" @@ -133,15 +132,6 @@ type Server struct { CancelWorker context.CancelFunc } -// Config holds config for server that isn't passed in by the user. -type Config struct { - AtlantisURLFlag string - AtlantisVersion string - DefaultTFVersionFlag string - RepoConfigJSONFlag string - AppCfg githubapp.Config -} - // WebhookConfig is nested within UserConfig. It's used to configure webhooks. type WebhookConfig struct { // Event is the type of event we should send this webhook for, ex. apply. @@ -160,8 +150,8 @@ type WebhookConfig struct { // NewServer returns a new server. If there are issues starting the server or // its dependencies an error will be returned. This is like the main() function // for the server CLI command because it injects all the dependencies. -func NewServer(userConfig UserConfig, config Config) (*Server, error) { - ctxLogger, err := logging.NewLoggerFromLevel(userConfig.ToLogLevel()) +func NewServer(userConfig UserConfig, appCfg githubapp.Config) (*Server, error) { + ctxLogger, err := logging.NewLoggerFromLevel(userConfig.LogLevel) if err != nil { return nil, err } @@ -195,7 +185,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } else if userConfig.RepoConfigJSON != "" { globalCfg, err = validator.ParseGlobalCfgJSON(userConfig.RepoConfigJSON, globalCfg) if err != nil { - return nil, errors.Wrapf(err, "parsing --%s", config.RepoConfigJSONFlag) + return nil, errors.Wrapf(err, "parsing %s file", userConfig.RepoConfigJSON) } } @@ -212,38 +202,44 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Regexes: globalCfg.TerraformLogFilter.Regexes, } - if userConfig.GithubUser != "" || userConfig.GithubAppID != 0 { + if userConfig.GithubSecrets.User != "" || userConfig.GithubSecrets.AppID != 0 { supportedVCSHosts = append(supportedVCSHosts, models.Github) - if userConfig.GithubUser != "" { + if userConfig.GithubSecrets.User != "" { githubCredentials = &vcs.GithubUserCredentials{ - User: userConfig.GithubUser, - Token: userConfig.GithubToken, + User: string(userConfig.GithubSecrets.User), + Token: userConfig.GithubSecrets.Token, } - } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKeyFile != "" { - privateKey, err := os.ReadFile(userConfig.GithubAppKeyFile) + } else if userConfig.GithubSecrets.AppID != 0 && userConfig.GithubSecrets.AppKeyFile != "" { + privateKey, err := os.ReadFile(userConfig.GithubSecrets.AppKeyFile) if err != nil { return nil, err } githubCredentials = &vcs.GithubAppCredentials{ - AppID: userConfig.GithubAppID, + AppID: userConfig.GithubSecrets.AppID, Key: privateKey, - Hostname: userConfig.GithubHostname, - AppSlug: userConfig.GithubAppSlug, + Hostname: userConfig.GithubSecrets.Hostname.String(), + AppSlug: userConfig.GithubSecrets.AppSlug, } githubAppEnabled = true - } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKey != "" { + } else if userConfig.GithubSecrets.AppID != 0 && userConfig.GithubSecrets.AppKey != "" { githubCredentials = &vcs.GithubAppCredentials{ - AppID: userConfig.GithubAppID, - Key: []byte(userConfig.GithubAppKey), - Hostname: userConfig.GithubHostname, - AppSlug: userConfig.GithubAppSlug, + AppID: userConfig.GithubSecrets.AppID, + Key: []byte(userConfig.GithubSecrets.AppKey), + Hostname: userConfig.GithubSecrets.Hostname.String(), + AppSlug: userConfig.GithubSecrets.AppSlug, } githubAppEnabled = true } var err error - rawGithubClient, err = vcs.NewGithubClient(userConfig.GithubHostname, githubCredentials, ctxLogger, mergeabilityChecker) + fmt.Printf("%+v", userConfig.GithubSecrets.Hostname.String()) + + rawGithubClient, err = vcs.NewGithubClient( + userConfig.GithubSecrets.Hostname.String(), + githubCredentials, + ctxLogger, + mergeabilityChecker) if err != nil { return nil, err } @@ -262,40 +258,46 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { githubClient = vcs.NewInstrumentedGithubClient(rawGithubClient, statsScope, ctxLogger) } - if userConfig.GitlabUser != "" { + if userConfig.GitlabSecrets.User != "" { supportedVCSHosts = append(supportedVCSHosts, models.Gitlab) var err error - gitlabClient, err = vcs.NewGitlabClient(userConfig.GitlabHostname, userConfig.GitlabToken, ctxLogger) + gitlabClient, err = vcs.NewGitlabClient(userConfig.GitlabSecrets.Hostname.String(), userConfig.GitlabSecrets.Token, ctxLogger) if err != nil { return nil, err } } - if userConfig.BitbucketUser != "" { - if userConfig.BitbucketBaseURL == bitbucketcloud.BaseURL { + if userConfig.BitbucketSecrets.User != "" { + if userConfig.BitbucketSecrets.BaseURL.String() == bitbucketcloud.BaseURL { supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud) bitbucketCloudClient = bitbucketcloud.NewClient( http.DefaultClient, - userConfig.BitbucketUser, - userConfig.BitbucketToken, - userConfig.AtlantisURL) + string(userConfig.BitbucketSecrets.User), + userConfig.BitbucketSecrets.Token, + userConfig.BitbucketSecrets.BaseURL.String(), + ) } else { supportedVCSHosts = append(supportedVCSHosts, models.BitbucketServer) var err error bitbucketServerClient, err = bitbucketserver.NewClient( http.DefaultClient, - userConfig.BitbucketUser, - userConfig.BitbucketToken, - userConfig.BitbucketBaseURL, - userConfig.AtlantisURL) + string(userConfig.BitbucketSecrets.User), + userConfig.BitbucketSecrets.Token, + userConfig.BitbucketSecrets.BaseURL.String(), + userConfig.AtlantisURL.String(), + ) if err != nil { return nil, errors.Wrapf(err, "setting up Bitbucket Server client") } } } - if userConfig.AzureDevopsUser != "" { + if userConfig.AzureDevopsSecrets.User != "" { supportedVCSHosts = append(supportedVCSHosts, models.AzureDevops) var err error - azuredevopsClient, err = vcs.NewAzureDevopsClient("dev.azure.com", userConfig.AzureDevopsUser, userConfig.AzureDevopsToken) + azuredevopsClient, err = vcs.NewAzureDevopsClient( + "dev.azure.com", + string(userConfig.AzureDevopsSecrets.User), + userConfig.AzureDevopsSecrets.Token, + ) if err != nil { return nil, err } @@ -306,29 +308,53 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file") } - if userConfig.GithubUser != "" { - if err := events.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, ctxLogger, false); err != nil { + if userConfig.GithubSecrets.User != "" { + if err := events.WriteGitCreds( + string(userConfig.GithubSecrets.User), + userConfig.GithubSecrets.Token, + userConfig.GithubSecrets.Hostname.String(), + home, + ctxLogger, + false); err != nil { return nil, err } } - if userConfig.GitlabUser != "" { - if err := events.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, ctxLogger, false); err != nil { + if userConfig.GitlabSecrets.User != "" { + if err := events.WriteGitCreds( + string(userConfig.GitlabSecrets.User), + userConfig.GitlabSecrets.Token, + userConfig.GitlabSecrets.Hostname.String(), + home, + ctxLogger, + false); err != nil { return nil, err } } - if userConfig.BitbucketUser != "" { + if userConfig.BitbucketSecrets.User != "" { // The default BitbucketBaseURL is https://api.bitbucket.org which can't actually be used for git // so we override it here only if it's that to be bitbucket.org - bitbucketBaseURL := userConfig.BitbucketBaseURL + bitbucketBaseURL := userConfig.BitbucketSecrets.BaseURL.String() if bitbucketBaseURL == "https://api.bitbucket.org" { bitbucketBaseURL = "bitbucket.org" } - if err := events.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, bitbucketBaseURL, home, ctxLogger, false); err != nil { + if err := events.WriteGitCreds( + string(userConfig.BitbucketSecrets.User), + userConfig.BitbucketSecrets.Token, + bitbucketBaseURL, + home, + ctxLogger, + false); err != nil { return nil, err } } - if userConfig.AzureDevopsUser != "" { - if err := events.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, "dev.azure.com", home, ctxLogger, false); err != nil { + if userConfig.AzureDevopsSecrets.User != "" { + if err := events.WriteGitCreds( + string(userConfig.AzureDevopsSecrets.User), + userConfig.AzureDevopsSecrets.Token, + "dev.azure.com", + home, + ctxLogger, + false); err != nil { return nil, err } } @@ -354,7 +380,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { TitleBuilder: vcs.StatusTitleBuilder{ TitlePrefix: userConfig.VCSStatusName, }, - DefaultDetailsURL: userConfig.DefaultCheckrunDetailsURL, + DefaultDetailsURL: userConfig.DefaultCheckrunDetailsURL.String(), } binDir, err := mkSubDir(userConfig.DataDir, BinDirName) @@ -369,15 +395,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { return nil, err } - parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL) - if err != nil { - return nil, errors.Wrapf(err, - "parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL) - } - underlyingRouter := mux.NewRouter() router := &Router{ - AtlantisURL: parsedURL, + AtlantisURL: userConfig.AtlantisURL.URL, LockViewRouteIDQueryParam: LockViewRouteIDQueryParam, LockViewRouteName: LockViewRouteName, ProjectJobsViewRouteName: ProjectJobsViewRouteName, @@ -413,8 +433,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { binDir, cacheDir, userConfig.DefaultTFVersion, - config.DefaultTFVersionFlag, - userConfig.TFDownloadURL, + userConfig.TFDownloadURL.String(), &terraform.DefaultDownloader{}, true, projectCmdOutputHandler) @@ -459,7 +478,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { workingDir = &events.GithubAppWorkingDir{ WorkingDir: workingDir, Credentials: githubCredentials, - GithubHostname: userConfig.GithubHostname, + GithubHostname: userConfig.GithubSecrets.Hostname.String(), } } @@ -491,22 +510,22 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ) eventParser := &events.EventParser{ - GithubUser: userConfig.GithubUser, - GithubToken: userConfig.GithubToken, - GitlabUser: userConfig.GitlabUser, - GitlabToken: userConfig.GitlabToken, - AllowDraftPRs: userConfig.PlanDrafts, - BitbucketUser: userConfig.BitbucketUser, - BitbucketToken: userConfig.BitbucketToken, - BitbucketServerURL: userConfig.BitbucketBaseURL, - AzureDevopsUser: userConfig.AzureDevopsUser, - AzureDevopsToken: userConfig.AzureDevopsToken, + GithubUser: string(userConfig.GithubSecrets.User), + GithubToken: userConfig.GithubSecrets.Token, + GitlabUser: string(userConfig.GitlabSecrets.User), + GitlabToken: userConfig.GitlabSecrets.Token, + AllowDraftPRs: userConfig.AllowDraftPrs, + BitbucketUser: string(userConfig.BitbucketSecrets.User), + BitbucketToken: userConfig.BitbucketSecrets.Token, + BitbucketServerURL: userConfig.BitbucketSecrets.BaseURL.String(), + AzureDevopsUser: string(userConfig.AzureDevopsSecrets.User), + AzureDevopsToken: userConfig.AzureDevopsSecrets.Token, } commentParser := &events.CommentParser{ - GithubUser: userConfig.GithubUser, - GitlabUser: userConfig.GitlabUser, - BitbucketUser: userConfig.BitbucketUser, - AzureDevopsUser: userConfig.AzureDevopsUser, + GithubUser: string(userConfig.GithubSecrets.User), + GitlabUser: string(userConfig.GitlabSecrets.User), + BitbucketUser: string(userConfig.BitbucketSecrets.User), + AzureDevopsUser: string(userConfig.AzureDevopsSecrets.User), ApplyDisabled: userConfig.DisableApply, } defaultTfVersion := terraformClient.DefaultVersion() @@ -552,8 +571,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { workingDirLocker, globalCfg, pendingPlanFinder, - userConfig.EnableRegExpCmd, - userConfig.AutoplanFileList, + userConfig.EnableRegexpCmd, + userConfig.AutoplanFileList.PatternMatcher, ctxLogger, userConfig.MaxProjectsPerPR, ) @@ -580,7 +599,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } clientCreator, err := githubapp.NewDefaultCachingClientCreator( - config.AppCfg, + appCfg, githubapp.WithClientMiddleware( middleware.ClientMetrics(statsScope.SubScope("github")), )) @@ -815,13 +834,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Logger: ctxLogger, } - repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist) + allowList := []string{} + + for _, repoAllowStmt := range userConfig.RepoAllowlist { + allowList = append(allowList, repoAllowStmt.String()) + } + + repoAllowlist, err := events.NewRepoAllowlistChecker(allowList) if err != nil { return nil, err } locksController := &controllers.LocksController{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, + AtlantisVersion: userConfig.AtlantisVersion, + AtlantisURL: userConfig.AtlantisURL.URL, Locker: lockingClient, ApplyLocker: applyLockingClient, Logger: ctxLogger, @@ -843,8 +868,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ) jobsController := &controllers.JobsController{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, + AtlantisVersion: userConfig.AtlantisVersion, + AtlantisURL: userConfig.AtlantisURL.URL, Logger: ctxLogger, ProjectJobsTemplate: templates.ProjectJobsTemplate, ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate, @@ -854,11 +879,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { KeyGenerator: controllers.JobIDKeyGenerator{}, } githubAppController := &controllers.GithubAppController{ - AtlantisURL: parsedURL, + AtlantisURL: userConfig.AtlantisURL.URL, Logger: ctxLogger, GithubSetupComplete: githubAppEnabled, - GithubHostname: userConfig.GithubHostname, - GithubOrg: userConfig.GithubOrg, + GithubHostname: userConfig.GithubSecrets.Hostname.String(), + GithubOrg: userConfig.GithubSecrets.Org, GithubStatusName: userConfig.VCSStatusName, } @@ -902,8 +927,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ctx, cancel := context.WithCancel(context.Background()) repoConverter := github_converter.RepoConverter{ - GithubUser: userConfig.GithubUser, - GithubToken: userConfig.GithubToken, + GithubUser: string(userConfig.GithubSecrets.User), + GithubToken: userConfig.GithubSecrets.Token, } pullConverter := github_converter.PullConverter{ @@ -912,8 +937,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { defaultEventsController := events_controllers.NewVCSEventsController( statsScope, - []byte(userConfig.GithubWebhookSecret), - userConfig.PlanDrafts, + []byte(userConfig.GithubSecrets.WebhookSecret), + userConfig.AllowDraftPrs, forceApplyCommandRunner, commentParser, eventParser, @@ -922,11 +947,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { vcsClient, ctxLogger, userConfig.DisableApply, - []byte(userConfig.GitlabWebhookSecret), + []byte(userConfig.GitlabSecrets.WebhookSecret), supportedVCSHosts, - []byte(userConfig.BitbucketWebhookSecret), - []byte(userConfig.AzureDevopsWebhookUser), - []byte(userConfig.AzureDevopsWebhookPassword), + []byte(userConfig.BitbucketSecrets.WebhookSecret), + []byte(userConfig.AzureDevopsSecrets.WebhookUser), + []byte(userConfig.AzureDevopsSecrets.WebhookPassword), repoConverter, pullConverter, githubClient, @@ -935,13 +960,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ) var vcsPostHandler sqs.VCSPostHandler - lyftMode := userConfig.ToLyftMode() - switch lyftMode { + switch userConfig.LyftMode { case Default: // default eventsController handles POST vcsPostHandler = defaultEventsController ctxLogger.Info("running Atlantis in default mode") case Worker: // an SQS worker is set up to handle messages via default eventsController - worker, err := sqs.NewGatewaySQSWorker(ctx, statsScope, ctxLogger, userConfig.LyftWorkerQueueURL, defaultEventsController) + worker, err := sqs.NewGatewaySQSWorker(ctx, statsScope, ctxLogger, userConfig.LyftWorkerQueueURL.String(), defaultEventsController) if err != nil { ctxLogger.Error("unable to set up worker", map[string]interface{}{ "err": err, @@ -956,8 +980,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } return &Server{ - AtlantisVersion: config.AtlantisVersion, - AtlantisURL: parsedURL, + AtlantisVersion: userConfig.AtlantisVersion, + AtlantisURL: userConfig.AtlantisURL.URL, Router: underlyingRouter, Port: userConfig.Port, PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, @@ -976,12 +1000,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { LockDetailTemplate: templates.LockTemplate, ProjectJobsTemplate: templates.ProjectJobsTemplate, ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate, - SSLKeyFile: userConfig.SSLKeyFile, - SSLCertFile: userConfig.SSLCertFile, + SSLKeyFile: userConfig.SSLSecrets.KeyFile, + SSLCertFile: userConfig.SSLSecrets.CertFile, Drainer: drainer, ScheduledExecutorService: scheduledExecutorService, ProjectCmdOutputHandler: projectCmdOutputHandler, - LyftMode: lyftMode, + LyftMode: userConfig.LyftMode, CancelWorker: cancel, }, nil } @@ -1162,21 +1186,3 @@ func (s *Server) Healthz(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write(data) // nolint: errcheck } - -// ParseAtlantisURL parses the user-passed atlantis URL to ensure it is valid -// and we can use it in our templates. -// It removes any trailing slashes from the path so we can concatenate it -// with other paths without checking. -func ParseAtlantisURL(u string) (*url.URL, error) { - parsed, err := url.Parse(u) - if err != nil { - return nil, err - } - if !(parsed.Scheme == "http" || parsed.Scheme == "https") { - return nil, errors.New("http or https must be specified") - } - // We want the path to end without a trailing slash so we know how to - // use it in the rest of the program. - parsed.Path = strings.TrimSuffix(parsed.Path, "/") - return parsed, nil -} diff --git a/server/server_test.go b/server/server_test.go index a49228714..461b45273 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -121,83 +121,3 @@ func TestHealthz(t *testing.T) { "status": "ok" }`, string(body)) } - -func TestParseAtlantisURL(t *testing.T) { - cases := []struct { - In string - ExpErr string - ExpURL string - }{ - // Valid URLs should work. - { - In: "https://example.com", - ExpURL: "https://example.com", - }, - { - In: "http://example.com", - ExpURL: "http://example.com", - }, - { - In: "http://example.com/", - ExpURL: "http://example.com", - }, - { - In: "http://example.com", - ExpURL: "http://example.com", - }, - { - In: "http://example.com:4141", - ExpURL: "http://example.com:4141", - }, - { - In: "http://example.com:4141/", - ExpURL: "http://example.com:4141", - }, - { - In: "http://example.com/baseurl", - ExpURL: "http://example.com/baseurl", - }, - { - In: "http://example.com/baseurl/", - ExpURL: "http://example.com/baseurl", - }, - { - In: "http://example.com/baseurl/test", - ExpURL: "http://example.com/baseurl/test", - }, - - // Must be valid URL. - { - In: "::", - ExpErr: "parse \"::\": missing protocol scheme", - }, - - // Must be absolute. - { - In: "/hi", - ExpErr: "http or https must be specified", - }, - - // Must have http or https scheme.. - { - In: "localhost/test", - ExpErr: "http or https must be specified", - }, - { - In: "http0://localhost/test", - ExpErr: "http or https must be specified", - }, - } - - for _, c := range cases { - t.Run(c.In, func(t *testing.T) { - act, err := server.ParseAtlantisURL(c.In) - if c.ExpErr != "" { - ErrEquals(t, c.ExpErr, err) - } else { - Ok(t, err) - Equals(t, c.ExpURL, act.String()) - } - }) - } -} diff --git a/server/user_config.go b/server/user_config.go index 23fb64ca9..012503785 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -1,9 +1,138 @@ package server import ( + "fmt" + "net/url" + "path/filepath" + "reflect" + "strings" + + "github.com/alecthomas/kong" + kongyaml "github.com/alecthomas/kong-yaml" + "github.com/docker/docker/pkg/fileutils" + "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" "github.com/runatlantis/atlantis/server/logging" ) +type ConfigFlag string + +func (c ConfigFlag) BeforeResolve(kongCli *kong.Kong, ctx *kong.Context, trace *kong.Path) error { + path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) + if path == "" { + return nil + } + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".yaml", ".yml": + kong.Configuration(kongyaml.Loader).Apply(kongCli) + case ".json": + kong.Configuration(kong.JSON).Apply(kongCli) + default: + return fmt.Errorf("no loader for config with extension %q found", ext) + } + resolver, err := kongCli.LoadConfig(path) + if err != nil { + return err + } + ctx.AddResolver(resolver) + return nil +} + +// URL with http or https schema +type HttpUrl struct { + *url.URL +} + +func (h *HttpUrl) Decode(ctx *kong.DecodeContext) error { + var rawUrl string + err := ctx.Scan.PopValueInto("string", &rawUrl) + if err != nil { + return err + } + parsedUrl, err := url.Parse(rawUrl) + if err != nil { + return err + } + if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" { + return fmt.Errorf("failed to parse HTTP url: protocol %q is not supported", parsedUrl.Scheme) + } + *h = HttpUrl{parsedUrl} + return nil +} + +func (h *HttpUrl) String() string { + if h != nil && h.URL != nil { + return h.URL.String() + } + return "" +} + +// URL object without schema +type Schemeless struct { + *url.URL +} + +func (s *Schemeless) Decode(ctx *kong.DecodeContext) error { + var rawUrl string + err := ctx.Scan.PopValueInto("string", &rawUrl) + if err != nil { + return err + } + parsedUrl, err := url.Parse(rawUrl) + if err != nil { + return err + } + if parsedUrl.Host == "" { + parsedUrl, err = url.Parse(fmt.Sprintf("//%s", rawUrl)) + if err != nil { + return err + } + } + parsedUrl.Scheme = "" + *s = Schemeless{parsedUrl} + return nil +} + +func (s *Schemeless) String() string { + if s != nil && s.URL != nil { + return s.URL.String() + } + return "" +} + +// Pattern path matcher +type Matcher struct { + *fileutils.PatternMatcher +} + +func (m *Matcher) Decode(ctx *kong.DecodeContext) error { + var rawFilesList string + err := ctx.Scan.PopValueInto("string", &rawFilesList) + if err != nil { + return err + } + patternMatcher, err := fileutils.NewPatternMatcher(strings.Split(rawFilesList, ",")) + if err != nil { + return err + } + *m = Matcher{patternMatcher} + return nil +} + +// VCS user +type User string + +func (u *User) Decode(ctx *kong.DecodeContext) error { + var rawUser string + err := ctx.Scan.PopValueInto("string", &rawUser) + if err != nil { + return err + } + *u = User(strings.TrimPrefix(rawUser, "@")) + return nil +} + +// Lyft Mode type Mode int const ( @@ -13,109 +142,154 @@ const ( TemporalWorker ) -// UserConfig holds config values passed in by the user. -// The mapstructure tags correspond to flags in cmd/server.go and are used when -// the config is parsed from a YAML file. -type UserConfig struct { - AtlantisURL string `mapstructure:"atlantis-url"` - AutoplanFileList string `mapstructure:"autoplan-file-list"` - AzureDevopsToken string `mapstructure:"azuredevops-token"` - AzureDevopsUser string `mapstructure:"azuredevops-user"` - AzureDevopsWebhookPassword string `mapstructure:"azuredevops-webhook-password"` - AzureDevopsWebhookUser string `mapstructure:"azuredevops-webhook-user"` - BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` - BitbucketToken string `mapstructure:"bitbucket-token"` - BitbucketUser string `mapstructure:"bitbucket-user"` - BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` - CheckoutStrategy string `mapstructure:"checkout-strategy"` - DataDir string `mapstructure:"data-dir"` - DisableApplyAll bool `mapstructure:"disable-apply-all"` - DisableApply bool `mapstructure:"disable-apply"` - DisableAutoplan bool `mapstructure:"disable-autoplan"` - DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"` - EnablePolicyChecks bool `mapstructure:"enable-policy-checks"` - EnableRegExpCmd bool `mapstructure:"enable-regexp-cmd"` - EnableDiffMarkdownFormat bool `mapstructure:"enable-diff-markdown-format"` - FFOwner string `mapstructure:"ff-owner"` - FFRepo string `mapstructure:"ff-repo"` - FFBranch string `mapstructure:"ff-branch"` - FFPath string `mapstructure:"ff-path"` - GithubHostname string `mapstructure:"gh-hostname"` - GithubToken string `mapstructure:"gh-token"` - GithubUser string `mapstructure:"gh-user"` - GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` - GithubOrg string `mapstructure:"gh-org"` - GithubAppID int64 `mapstructure:"gh-app-id"` - GithubAppKey string `mapstructure:"gh-app-key"` - GithubAppKeyFile string `mapstructure:"gh-app-key-file"` - GithubAppSlug string `mapstructure:"gh-app-slug"` - GitlabHostname string `mapstructure:"gitlab-hostname"` - GitlabToken string `mapstructure:"gitlab-token"` - GitlabUser string `mapstructure:"gitlab-user"` - GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` - HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"` - LogLevel string `mapstructure:"log-level"` - ParallelPoolSize int `mapstructure:"parallel-pool-size"` - MaxProjectsPerPR int `mapstructure:"max-projects-per-pr"` - StatsNamespace string `mapstructure:"stats-namespace"` - PlanDrafts bool `mapstructure:"allow-draft-prs"` - Port int `mapstructure:"port"` - RepoConfig string `mapstructure:"repo-config"` - RepoConfigJSON string `mapstructure:"repo-config-json"` - RepoAllowlist string `mapstructure:"repo-allowlist"` - // RepoWhitelist is deprecated in favour of RepoAllowlist. - RepoWhitelist string `mapstructure:"repo-whitelist"` - - // RequireUnDiverged is whether to require pull requests to rebase default branch before - // allowing terraform apply's to run. - RequireUnDiverged bool `mapstructure:"require-undiverged"` - // RequireSQUnlocked is whether to require pull requests to be unlocked before running - // terraform apply. - RequireSQUnlocked bool `mapstructure:"require-unlocked"` - SlackToken string `mapstructure:"slack-token"` - SSLCertFile string `mapstructure:"ssl-cert-file"` - SSLKeyFile string `mapstructure:"ssl-key-file"` - TFDownloadURL string `mapstructure:"tf-download-url"` - VCSStatusName string `mapstructure:"vcs-status-name"` - DefaultTFVersion string `mapstructure:"default-tf-version"` - Webhooks []WebhookConfig `mapstructure:"webhooks"` - WriteGitCreds bool `mapstructure:"write-git-creds"` - LyftAuditJobsSnsTopicArn string `mapstructure:"lyft-audit-jobs-sns-topic-arn"` - LyftGatewaySnsTopicArn string `mapstructure:"lyft-gateway-sns-topic-arn"` - LyftMode string `mapstructure:"lyft-mode"` - LyftWorkerQueueURL string `mapstructure:"lyft-worker-queue-url"` - - // Supports adding a default URL to the checkrun UI when details URL is not set - DefaultCheckrunDetailsURL string `mapstructure:"default-checkrun-details-url"` -} - -// ToLogLevel returns the LogLevel object corresponding to the user-passed -// log level. -func (u UserConfig) ToLogLevel() logging.LogLevel { - switch u.LogLevel { - case "debug": - return logging.Debug - case "info": - return logging.Info - case "warn": - return logging.Warn - case "error": - return logging.Error - } - return logging.Info -} - -// ToLyftMode returns mode type to run atlantis on. -func (u UserConfig) ToLyftMode() Mode { - switch u.LyftMode { +func (m *Mode) Decode(ctx *kong.DecodeContext) error { + var rawMode string + err := ctx.Scan.PopValueInto("string", &rawMode) + if err != nil { + return err + } + switch strings.ToLower(rawMode) { case "default": - return Default + ctx.Value.Target.Set(reflect.ValueOf(Default)) case "gateway": - return Gateway + ctx.Value.Target.Set(reflect.ValueOf(Gateway)) case "worker": - return Worker + ctx.Value.Target.Set(reflect.ValueOf(Worker)) case "temporalworker": - return TemporalWorker + ctx.Value.Target.Set(reflect.ValueOf(TemporalWorker)) + default: + return fmt.Errorf("Lyft mode %q is not supported", rawMode) + } + return nil +} + +// Atlantis HTTPS SSL secrets +type SSLSecrets struct { + CertFile string `type:"filecontent" help:"${help_ssl_cert_file}"` + KeyFile string `help:"${help_ssl_key_file}"` +} + +func (s SSLSecrets) Validate() error { + if (s.KeyFile == "") != (s.CertFile == "") { + return fmt.Errorf("both ssl key and certificate are required") + } + return nil +} + +// VCS Azure Devops secrets +type AzureDevopsSecrets struct { + WebhookPassword string `help:"${help_ad_webhook_password}"` + WebhookUser string `help:"${help_ad_webhook_user}"` + Token string `help:"${help_ad_token}"` // nolint: gosec + User User `help:"${help_ad_user}"` +} + +func (s AzureDevopsSecrets) Validate() error { + if (s.User == "") != (s.Token == "") { + return fmt.Errorf("AzureDevops: both user and token should be set") + } + return nil +} + +// VCS Bitbucket secrets +type BitbucketSecrets struct { + BaseURL HttpUrl `help:"${help_bitbucket_base_url}" default:"${default_bitbucket_base_url}"` + Token string `help:"${help_bitbucket_token}"` + User User `help:"${help_bitbucket_user}"` + WebhookSecret string `help:"${help_bitbucket_webhook_secret}"` +} + +func (s BitbucketSecrets) Validate() error { + if (s.User == "") != (s.Token == "") { + return fmt.Errorf("Bitbucket: both user and token should be set") + } + if s.BaseURL.String() == bitbucketcloud.BaseURL && s.WebhookSecret != "" { + return fmt.Errorf("Bitbucket: webhook secret for Bitbucket Cloud is not supported") + } + return nil +} + +// VCS Github secrets +type GithubSecrets struct { + Hostname Schemeless `default:"github.com" help:"${help_gh_hostname}"` + Token string `help:"${help_gh_token}"` + User User `help:"${help_gh_user}"` + AppID int64 `help:"${help_gh_app_id}"` + AppKey string `help:"${help_gh_app_key}"` + AppKeyFile string `type:"filecontent" help:"${help_gh_app_key_file}"` + AppSlug string `help:"${help_gh_app_slug}"` + Org string `help:"${help_gh_organization}"` + WebhookSecret string `help:"${help_gh_webhook_secret}"` // nolint: gosec +} + +func (s GithubSecrets) Validate() error { + if (s.User == "") != (s.Token == "") { + return fmt.Errorf("Github: both user and token should be set") + } + if (s.AppID == 0) != (s.AppKey == "" && s.AppKeyFile == "") { + return fmt.Errorf("Github: either app key or app key file should be set together with app ID") } - return Default + return nil +} + +// VCS Gitlab secrets +type GitlabSecrets struct { + Hostname Schemeless `default:"gitlab.com" help:"${help_gitlab_hostname}"` + Token string `help:"${help_gitlab_token}"` + User User `help:"${help_gitlab_user}"` + WebhookSecret string `help:"${help_gitlab_webhook_secret}"` // nolint: gosec +} + +func (s GitlabSecrets) Validate() error { + if (s.User == "") != (s.Token == "") { + return fmt.Errorf("Gitlab: both user and token should be set") + } + return nil +} + +type UserConfig struct { + Silence bool `short:"s" help:"Silence Atlantis warnings"` + AtlantisURL HttpUrl `help:"${help_atlantis_url}"` + AutoplanFileList Matcher `default:"${default_autoplan_file_list}" help:"${help_autoplan_file_list}"` + Config ConfigFlag `help:"${help_config}"` + CheckoutStrategy string `default:"branch" enum:"branch,merge" help:"${help_checkout_strategy}"` + DataDir string `type:"path" default:"~/.atlantis" help:"${help_data_dir}"` + DefaultTFVersion string `help:"${help_default_tf_version}"` + DisableApplyAll bool `help:"${help_disable_apply_all}"` + DisableApply bool `help:"${help_disable_apply}"` + DisableAutoplan bool `help:"${help_disable_autoplan}"` + DisableMarkdownFolding bool `help:"${help_disable_markdown_folding}"` + EnableRegexpCmd bool `help:"${help_enable_regexp_cmd}"` + EnableDiffMarkdownFormat bool `help:"${help_enable_diff_markdown_format}"` + EnablePolicyChecks bool `help:"${help_enable_policy_checks}"` + FFOwner string `help:"${help_ff_owner}"` + FFRepo string `help:"${help_ff_repo}"` + FFBranch string `help:"${help_ff_branch}"` + FFPath string `help:"${help_ff_path}"` + HidePrevPlanComments bool `help:"${help_hide_prev_plan_comments}"` + LogLevel logging.LogLevel `default:"info" enuam:"debug,info,warn,error" help:"${help_log_level}"` + ParallelPoolSize int `default:"15" help:"${help_parallel_pool_size}"` + MaxProjectsPerPR int `help:"${help_max_projects_per_pr}"` + StatsNamespace string `default:"atlantis" help:"${help_stats_namespace}"` + AllowDraftPrs bool `help:"${help_allow_draft_prs}"` + Port int `default:"4141" help:"${help_port}"` + RepoConfig string `help:"${help_repo_config}"` + RepoConfigJSON string `help:"${help_repo_config_json}"` + RepoAllowlist []Schemeless `help:"${help_repo_allowlist}" required` + SlackToken string `help:"${help_slack_token}"` + TFDownloadURL HttpUrl `default:"https://releases.hashicorp.com" help:"${help_tf_download_url}"` + VCSStatusName string `default:"atlantis" help:"${help_vcs_status_name}"` + WriteGitCreds bool `help:"${help_write_git_creds}"` + LyftAuditJobsSnsTopicArn string `help:"${help_lyft_audit_jobs_sns_topic_arn}"` + LyftGatewaySnsTopicArn string `help:"${help_lyft_gateway_sns_topic_arn}"` + LyftMode Mode `enuam:"default,gateway,worker,hybrid" default:"default" help:"${help_lyft_mode}"` + LyftWorkerQueueURL HttpUrl `help:"${help_lyft_worker_queue_url}"` + DefaultCheckrunDetailsURL HttpUrl `help:"${help_default_checkrun_details_url}"` + AtlantisVersion string `kong:"-"` + Webhooks []WebhookConfig `kong:"-"` + SSLSecrets `kong:"embed,prefix='ssl-'"` + AzureDevopsSecrets `kong:"embed,prefix='azuredevops-'"` + GithubSecrets `kong:"embed,prefix='gh-'"` + GitlabSecrets `kong:"embed,prefix='gitlab-'"` + BitbucketSecrets `kong:"embed,prefix='bitbucket-'"` } diff --git a/server/user_config_test.go b/server/user_config_test.go deleted file mode 100644 index cb57671fd..000000000 --- a/server/user_config_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package server_test - -import ( - "testing" - - "github.com/runatlantis/atlantis/server" - "github.com/runatlantis/atlantis/server/logging" - . "github.com/runatlantis/atlantis/testing" -) - -func TestUserConfig_ToLogLevel(t *testing.T) { - cases := []struct { - userLvl string - expLvl logging.LogLevel - }{ - { - "debug", - logging.Debug, - }, - { - "info", - logging.Info, - }, - { - "warn", - logging.Warn, - }, - { - "error", - logging.Error, - }, - { - "unknown", - logging.Info, - }, - } - - for _, c := range cases { - t.Run(c.userLvl, func(t *testing.T) { - u := server.UserConfig{ - LogLevel: c.userLvl, - } - Equals(t, c.expLvl, u.ToLogLevel()) - }) - } -} - -func TestUserConfig_ToLyftMode(t *testing.T) { - cases := []struct { - userMode string - expMode server.Mode - }{ - { - "default", - server.Default, - }, - { - "gateway", - server.Gateway, - }, - { - "worker", - server.Worker, - }, - { - "unknown", - server.Default, - }, - { - "", - server.Default, - }, - } - - for _, c := range cases { - t.Run(c.userMode, func(t *testing.T) { - u := server.UserConfig{ - LyftMode: c.userMode, - } - Equals(t, c.expMode, u.ToLyftMode()) - }) - } -}