diff --git a/cmd/server.go b/cmd/server.go index d50d196a71..1cfcb2aecf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -15,7 +15,7 @@ import ( // 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.Config and set the mapstructure tag equal to the flag name. +// 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 ( AtlantisURLFlag = "atlantis-url" @@ -169,7 +169,7 @@ type ServerCmd struct { // ServerCreator creates servers. // It's an abstraction to help us test. type ServerCreator interface { - NewServer(config server.Config, flagNames server.FlagNames) (ServerStarter, error) + NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) } // DefaultServerCreator is the concrete implementation of ServerCreator. @@ -182,8 +182,8 @@ type ServerStarter interface { } // NewServer returns the real Atlantis server object. -func (d *DefaultServerCreator) NewServer(config server.Config, flagNames server.FlagNames) (ServerStarter, error) { - return server.NewServer(config, flagNames) +func (d *DefaultServerCreator) NewServer(userConfig server.UserConfig, config server.Config) (ServerStarter, error) { + return server.NewServer(userConfig, config) } // Init returns the runnable cobra command. @@ -252,25 +252,26 @@ func (s *ServerCmd) preRun() error { } func (s *ServerCmd) run() error { - var config server.Config - if err := s.Viper.Unmarshal(&config); err != nil { + var userConfig server.UserConfig + if err := s.Viper.Unmarshal(&userConfig); err != nil { return err } - if err := s.validate(config); err != nil { + if err := s.validate(userConfig); err != nil { return err } - if err := s.setAtlantisURL(&config); err != nil { + if err := s.setAtlantisURL(&userConfig); err != nil { return err } - if err := s.setDataDir(&config); err != nil { + if err := s.setDataDir(&userConfig); err != nil { return err } - s.securityWarnings(&config) - s.trimAtSymbolFromUsers(&config) + s.securityWarnings(&userConfig) + s.trimAtSymbolFromUsers(&userConfig) // Config looks good. Start the server. - server, err := s.ServerCreator.NewServer(config, server.FlagNames{ + server, err := s.ServerCreator.NewServer(userConfig, server.Config{ AllowForkPRsFlag: AllowForkPRsFlag, + AtlantisVersion: s.Viper.Get("version").(string), }) if err != nil { return errors.Wrap(err, "initializing server") @@ -279,13 +280,13 @@ func (s *ServerCmd) run() error { } // nolint: gocyclo -func (s *ServerCmd) validate(config server.Config) error { - logLevel := config.LogLevel +func (s *ServerCmd) validate(userConfig server.UserConfig) error { + logLevel := userConfig.LogLevel if logLevel != "debug" && logLevel != "info" && logLevel != "warn" && logLevel != "error" { return errors.New("invalid log level: not one of debug, info, warn, error") } - if (config.SSLKeyFile == "") != (config.SSLCertFile == "") { + if (userConfig.SSLKeyFile == "") != (userConfig.SSLCertFile == "") { return fmt.Errorf("--%s and --%s are both required for ssl", SSLKeyFileFlag, SSLCertFileFlag) } @@ -294,16 +295,16 @@ func (s *ServerCmd) validate(config server.Config) error { // 2. gitlab user and token set // 3. all 4 set vcsErr := fmt.Errorf("--%s and --%s or --%s and --%s must be set", GHUserFlag, GHTokenFlag, GitlabUserFlag, GitlabTokenFlag) - if ((config.GithubUser == "") != (config.GithubToken == "")) || ((config.GitlabUser == "") != (config.GitlabToken == "")) { + if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) || ((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) { 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 config.GithubUser == "" && config.GitlabUser == "" { + if userConfig.GithubUser == "" && userConfig.GitlabUser == "" { return vcsErr } - if config.RepoWhitelist == "" { + if userConfig.RepoWhitelist == "" { return fmt.Errorf("--%s must be set for security purposes", RepoWhitelistFlag) } @@ -311,13 +312,13 @@ func (s *ServerCmd) validate(config server.Config) error { } // setAtlantisURL sets the externally accessible URL for atlantis. -func (s *ServerCmd) setAtlantisURL(config *server.Config) error { - if config.AtlantisURL == "" { +func (s *ServerCmd) setAtlantisURL(userConfig *server.UserConfig) error { + if userConfig.AtlantisURL == "" { hostname, err := os.Hostname() if err != nil { return fmt.Errorf("Failed to determine hostname: %v", err) } - config.AtlantisURL = fmt.Sprintf("http://%s:%d", hostname, config.Port) + userConfig.AtlantisURL = fmt.Sprintf("http://%s:%d", hostname, userConfig.Port) } return nil } @@ -325,8 +326,8 @@ func (s *ServerCmd) setAtlantisURL(config *server.Config) error { // 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(config *server.Config) error { - finalPath := config.DataDir +func (s *ServerCmd) setDataDir(userConfig *server.UserConfig) error { + finalPath := userConfig.DataDir // Convert ~ to the actual home dir. if strings.HasPrefix(finalPath, "~/") { @@ -342,21 +343,21 @@ func (s *ServerCmd) setDataDir(config *server.Config) error { if err != nil { return errors.Wrap(err, "making data-dir absolute") } - config.DataDir = finalPath + userConfig.DataDir = finalPath return nil } // trimAtSymbolFromUsers trims @ from the front of the github and gitlab usernames -func (s *ServerCmd) trimAtSymbolFromUsers(config *server.Config) { - config.GithubUser = strings.TrimPrefix(config.GithubUser, "@") - config.GitlabUser = strings.TrimPrefix(config.GitlabUser, "@") +func (s *ServerCmd) trimAtSymbolFromUsers(userConfig *server.UserConfig) { + userConfig.GithubUser = strings.TrimPrefix(userConfig.GithubUser, "@") + userConfig.GitlabUser = strings.TrimPrefix(userConfig.GitlabUser, "@") } -func (s *ServerCmd) securityWarnings(config *server.Config) { - if config.GithubUser != "" && config.GithubWebHookSecret == "" && !s.SilenceOutput { +func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { + if userConfig.GithubUser != "" && userConfig.GithubWebHookSecret == "" && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] No GitHub webhook secret set. This could allow attackers to spoof requests from GitHub. See https://git.io/vAF3t%s\n", RedTermStart, RedTermEnd) } - if config.GitlabUser != "" && config.GitlabWebHookSecret == "" && !s.SilenceOutput { + if userConfig.GitlabUser != "" && userConfig.GitlabWebHookSecret == "" && !s.SilenceOutput { fmt.Fprintf(os.Stderr, "%s[WARN] No GitLab webhook secret set. This could allow attackers to spoof requests from GitLab. See https://git.io/vAF3t%s\n", RedTermStart, RedTermEnd) } } diff --git a/cmd/server_test.go b/cmd/server_test.go index d131799131..ca7ac50056 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -17,12 +17,12 @@ import ( // passedConfig is set to whatever config ended up being passed to NewServer. // Used for testing. -var passedConfig server.Config +var passedConfig server.UserConfig type ServerCreatorMock struct{} -func (s *ServerCreatorMock) NewServer(config server.Config, flagNames server.FlagNames) (cmd.ServerStarter, error) { - passedConfig = config +func (s *ServerCreatorMock) NewServer(userConfig server.UserConfig, config server.Config) (cmd.ServerStarter, error) { + passedConfig = userConfig return &ServerStarterMock{}, nil } @@ -47,7 +47,7 @@ func TestExecute_ConfigFileExtension(t *testing.T) { cmd.ConfigFlag: "does-not-exist", }) err := c.Execute() - Equals(t, "invalid config: reading does-not-exist: Unsupported Config Type \"\"", err.Error()) + Equals(t, "invalid config: reading does-not-exist: Unsupported UserConfig Type \"\"", err.Error()) } func TestExecute_ConfigFileMissing(t *testing.T) { diff --git a/server/server.go b/server/server.go index 278e14ccba..0650543993 100644 --- a/server/server.go +++ b/server/server.go @@ -36,6 +36,7 @@ const LockRouteName = "lock-detail" // Server runs the Atlantis web server. type Server struct { + AtlantisVersion string Router *mux.Router Port int CommandHandler *events.CommandHandler @@ -49,10 +50,10 @@ type Server struct { SSLKeyFile string } -// Config configures Server. +// UserConfig configures Server. // The mapstructure tags correspond to flags in cmd/server.go and are used when // the config is parsed from a YAML file. -type Config struct { +type UserConfig struct { AllowForkPRs bool `mapstructure:"allow-fork-prs"` AtlantisURL string `mapstructure:"atlantis-url"` DataDir string `mapstructure:"data-dir"` @@ -76,15 +77,16 @@ type Config struct { Webhooks []WebhookConfig `mapstructure:"webhooks"` } -// FlagNames contains the names of the flags available to atlantis server. +// Config contains the names of the flags available to atlantis server. // They're useful because sometimes we comment back asking the user to enable // a certain flag. -type FlagNames struct { +type Config struct { AllowForkPRsFlag string RepoWhitelistFlag string + AtlantisVersion string } -// WebhookConfig is nested within Config. It's used to configure webhooks. +// 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. Event string `mapstructure:"event"` @@ -102,41 +104,41 @@ 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(config Config, flagNames FlagNames) (*Server, error) { +func NewServer(userConfig UserConfig, config Config) (*Server, error) { var supportedVCSHosts []vcs.Host var githubClient *vcs.GithubClient var gitlabClient *vcs.GitlabClient - if config.GithubUser != "" { + if userConfig.GithubUser != "" { supportedVCSHosts = append(supportedVCSHosts, vcs.Github) var err error - githubClient, err = vcs.NewGithubClient(config.GithubHostname, config.GithubUser, config.GithubToken) + githubClient, err = vcs.NewGithubClient(userConfig.GithubHostname, userConfig.GithubUser, userConfig.GithubToken) if err != nil { return nil, err } } - if config.GitlabUser != "" { + if userConfig.GitlabUser != "" { supportedVCSHosts = append(supportedVCSHosts, vcs.Gitlab) gitlabClient = &vcs.GitlabClient{ - Client: gitlab.NewClient(nil, config.GitlabToken), + Client: gitlab.NewClient(nil, userConfig.GitlabToken), } // If not using gitlab.com we need to set the URL to the API. - if config.GitlabHostname != "gitlab.com" { + if userConfig.GitlabHostname != "gitlab.com" { // Check if they've also provided a scheme so we don't prepend it // again. scheme := "https" - schemeSplit := strings.Split(config.GitlabHostname, "://") + schemeSplit := strings.Split(userConfig.GitlabHostname, "://") if len(schemeSplit) > 1 { scheme = schemeSplit[0] - config.GitlabHostname = schemeSplit[1] + userConfig.GitlabHostname = schemeSplit[1] } - apiURL := fmt.Sprintf("%s://%s/api/v4/", scheme, config.GitlabHostname) + apiURL := fmt.Sprintf("%s://%s/api/v4/", scheme, userConfig.GitlabHostname) if err := gitlabClient.Client.SetBaseURL(apiURL); err != nil { return nil, errors.Wrapf(err, "setting GitLab API URL: %s", apiURL) } } } var webhooksConfig []webhooks.Config - for _, c := range config.Webhooks { + for _, c := range userConfig.Webhooks { config := webhooks.Config{ Channel: c.Channel, Event: c.Event, @@ -145,7 +147,7 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { } webhooksConfig = append(webhooksConfig, config) } - webhooksManager, err := webhooks.NewMultiWebhookSender(webhooksConfig, webhooks.NewSlackClient(config.SlackToken)) + webhooksManager, err := webhooks.NewMultiWebhookSender(webhooksConfig, webhooks.NewSlackClient(userConfig.SlackToken)) if err != nil { return nil, errors.Wrap(err, "initializing webhooks") } @@ -159,7 +161,7 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { return nil, errors.Wrap(err, "initializing terraform") } markdownRenderer := &events.MarkdownRenderer{} - boltdb, err := boltdb.New(config.DataDir) + boltdb, err := boltdb.New(userConfig.DataDir) if err != nil { return nil, err } @@ -168,7 +170,7 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { configReader := &events.ProjectConfigManager{} workspaceLocker := events.NewDefaultAtlantisWorkspaceLocker() workspace := &events.FileWorkspace{ - DataDir: config.DataDir, + DataDir: userConfig.DataDir, } projectPreExecute := &events.DefaultProjectPreExecutor{ Locker: lockingClient, @@ -179,7 +181,7 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { applyExecutor := &events.ApplyExecutor{ VCSClient: vcsClient, Terraform: terraformClient, - RequireApproval: config.RequireApproval, + RequireApproval: userConfig.RequireApproval, Run: run, AtlantisWorkspace: workspace, ProjectPreExecute: projectPreExecute, @@ -199,18 +201,18 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { Locker: lockingClient, Workspace: workspace, } - logger := logging.NewSimpleLogger("server", nil, false, logging.ToLogLevel(config.LogLevel)) + logger := logging.NewSimpleLogger("server", nil, false, logging.ToLogLevel(userConfig.LogLevel)) eventParser := &events.EventParser{ - GithubUser: config.GithubUser, - GithubToken: config.GithubToken, - GitlabUser: config.GitlabUser, - GitlabToken: config.GitlabToken, + GithubUser: userConfig.GithubUser, + GithubToken: userConfig.GithubToken, + GitlabUser: userConfig.GitlabUser, + GitlabToken: userConfig.GitlabToken, } commentParser := &events.CommentParser{ - GithubUser: config.GithubUser, - GithubToken: config.GithubToken, - GitlabUser: config.GitlabUser, - GitlabToken: config.GitlabToken, + GithubUser: userConfig.GithubUser, + GithubToken: userConfig.GithubToken, + GitlabUser: userConfig.GitlabUser, + GitlabToken: userConfig.GitlabToken, } commandHandler := &events.CommandHandler{ ApplyExecutor: applyExecutor, @@ -224,11 +226,11 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { AtlantisWorkspaceLocker: workspaceLocker, MarkdownRenderer: markdownRenderer, Logger: logger, - AllowForkPRs: config.AllowForkPRs, - AllowForkPRsFlag: flagNames.AllowForkPRsFlag, + AllowForkPRs: userConfig.AllowForkPRs, + AllowForkPRsFlag: config.AllowForkPRsFlag, } repoWhitelist := &events.RepoWhitelist{ - Whitelist: config.RepoWhitelist, + Whitelist: userConfig.RepoWhitelist, } eventsController := &EventsController{ CommandRunner: commandHandler, @@ -236,27 +238,28 @@ func NewServer(config Config, flagNames FlagNames) (*Server, error) { Parser: eventParser, CommentParser: commentParser, Logger: logger, - GithubWebHookSecret: []byte(config.GithubWebHookSecret), + GithubWebHookSecret: []byte(userConfig.GithubWebHookSecret), GithubRequestValidator: &DefaultGithubRequestValidator{}, GitlabRequestParser: &DefaultGitlabRequestParser{}, - GitlabWebHookSecret: []byte(config.GitlabWebHookSecret), + GitlabWebHookSecret: []byte(userConfig.GitlabWebHookSecret), RepoWhitelist: repoWhitelist, SupportedVCSHosts: supportedVCSHosts, VCSClient: vcsClient, } router := mux.NewRouter() return &Server{ + AtlantisVersion: config.AtlantisVersion, Router: router, - Port: config.Port, + Port: userConfig.Port, CommandHandler: commandHandler, Logger: logger, Locker: lockingClient, - AtlantisURL: config.AtlantisURL, + AtlantisURL: userConfig.AtlantisURL, EventsController: eventsController, IndexTemplate: indexTemplate, LockDetailTemplate: lockTemplate, - SSLKeyFile: config.SSLKeyFile, - SSLCertFile: config.SSLCertFile, + SSLKeyFile: userConfig.SSLKeyFile, + SSLCertFile: userConfig.SSLCertFile, }, nil } @@ -324,17 +327,20 @@ func (s *Server) Index(w http.ResponseWriter, _ *http.Request) { return } - var results []LockIndexData + var lockResults []LockIndexData for id, v := range locks { lockURL, _ := s.Router.Get(LockRouteName).URL("id", url.QueryEscape(id)) - results = append(results, LockIndexData{ + lockResults = append(lockResults, LockIndexData{ LockURL: lockURL.String(), RepoFullName: v.Project.RepoFullName, PullNum: v.Pull.Num, Time: v.Time, }) } - s.IndexTemplate.Execute(w, results) // nolint: errcheck + s.IndexTemplate.Execute(w, IndexData { + Locks: lockResults, + AtlantisVersion: s.AtlantisVersion, + }) // nolint: errcheck } // GetLockRoute is the GET /locks/{id} route. It renders the lock detail view. diff --git a/server/server_test.go b/server/server_test.go index a7d8e5f7ad..de3ac5cd90 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -24,9 +24,9 @@ func TestNewServer(t *testing.T) { t.Log("Run through NewServer constructor") tmpDir, err := ioutil.TempDir("", "") Ok(t, err) - _, err = server.NewServer(server.Config{ + _, err = server.NewServer(server.UserConfig{ DataDir: tmpDir, - }, server.FlagNames{}) + }, server.Config{}) Ok(t, err) } @@ -64,23 +64,28 @@ func TestIndex_Success(t *testing.T) { When(l.List()).ThenReturn(locks, nil) it := sMocks.NewMockTemplateWriter() r := mux.NewRouter() + atlantisVersion := "0.3.1" // Need to create a lock route since the server expects this route to exist. r.NewRoute().Path("").Name(server.LockRouteName) s := server.Server{ - Locker: l, - IndexTemplate: it, - Router: r, + Locker: l, + IndexTemplate: it, + Router: r, + AtlantisVersion: atlantisVersion, } eventsReq, _ = http.NewRequest("GET", "", bytes.NewBuffer(nil)) w := httptest.NewRecorder() s.Index(w, eventsReq) - it.VerifyWasCalledOnce().Execute(w, []server.LockIndexData{ - { - LockURL: "", - RepoFullName: "owner/repo", - PullNum: 9, - Time: now, + it.VerifyWasCalledOnce().Execute(w, server.IndexData{ + Locks: []server.LockIndexData{ + { + LockURL: "", + RepoFullName: "owner/repo", + PullNum: 9, + Time: now, + }, }, + AtlantisVersion: atlantisVersion, }) responseContains(t, w, http.StatusOK, "") } diff --git a/server/web_templates.go b/server/web_templates.go index a42a206f88..b450358bb7 100644 --- a/server/web_templates.go +++ b/server/web_templates.go @@ -24,6 +24,12 @@ type LockIndexData struct { Time time.Time } +// IndexData holds the data for rendering the index page +type IndexData struct { + Locks []LockIndexData + AtlantisVersion string +} + var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(` @@ -62,8 +68,8 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`

Locks

- {{ if . }} - {{ range . }} + {{ if .Locks }} + {{ range .Locks }}
{{.RepoFullName}} - #{{.PullNum}}