Skip to content

Commit

Permalink
feat: Add Gitea support (#4229)
Browse files Browse the repository at this point in the history
* Add initial Gitea client structure

* Add various missing config flags

* initial gitea support added

* Fix some post-merge issues

* Replace HidePrevCommandComments by version from @florianbeisel

* Update mocks

* feat: add Webhook Signature Verification

This changes adds support for Gitea Webhook Signatures by wrapping the
function from the Gitea SDK and calling it from `handleGiteaPost()`.

* fix: use release version in go.mod

1.22 as in the previous go.mod is a development version. When referencing
a minimum release version the correct format is 1.22.0

* Set default Gitea url to cloud.gitea.com

* Fix and Add tests for Gitea

* Fix missing copyright header

* Changed comment to reflect no max comment length

Apparently there's no max comment length in Gitea at this point in time.

* Implement GetCloneURL()

* Decode Base64 before passing on downloaded file content

* Enable Gitea client as API Client

* Remove unneded comments

* Remove old redundant file

* fix: invalid version number in go.mod

* fix: remove unnecessary type conversions

* fix: removed unused function

* fix: remove unnecessary type conversion of decodedData

* fix: fixes some tests

* Correct gitea.com URL

* Add Gitea to website docs

* fix: TestPost_UnsupportedGiteaEvent

* revert version downgrades

* docs: add Gitea documentation to Guide section

* docs: fix copy paste mistake

* Update cmd/server_test.go

Co-authored-by: nitrocode <[email protected]>

* Clarify usage msg for --gitea-base-url

* Apply suggestions from code review

Co-authored-by: nitrocode <[email protected]>

* Turn ebreak number into const with comments

* Add --gitea-page-size server argument

Defaults to 30 based on https://docs.gitea.com/1.18/advanced/config-cheat-sheet#api-api

* Fix broken test

* Fix event parser and comment parser

* Add missing app permission to docs

* Make Gitea client conform to updated interface

* Update server/events/vcs/gitea/client.go

Co-authored-by: Simon Heather <[email protected]>

* Remove no longer needed logger

* Add extra logging statements for Gitea client

* Add debug statements

---------

Co-authored-by: Florian Beisel <[email protected]>
Co-authored-by: Florian Beisel <[email protected]>
Co-authored-by: PePe Amengual <[email protected]>
Co-authored-by: nitrocode <[email protected]>
Co-authored-by: Rui Chen <[email protected]>
Co-authored-by: Simon Heather <[email protected]>
  • Loading branch information
7 people authored Mar 18, 2024
1 parent 2b02a43 commit 722c4a9
Show file tree
Hide file tree
Showing 25 changed files with 1,495 additions and 34 deletions.
66 changes: 59 additions & 7 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ const (
GHOrganizationFlag = "gh-org"
GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec
GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec
GiteaBaseURLFlag = "gitea-base-url"
GiteaTokenFlag = "gitea-token"
GiteaUserFlag = "gitea-user"
GiteaWebhookSecretFlag = "gitea-webhook-secret" // nolint: gosec
GiteaPageSizeFlag = "gitea-page-size"
GitlabHostnameFlag = "gitlab-hostname"
GitlabTokenFlag = "gitlab-token"
GitlabUserFlag = "gitlab-user"
Expand Down Expand Up @@ -156,6 +161,8 @@ const (
DefaultExecutableName = "atlantis"
DefaultMarkdownTemplateOverridesDir = "~/.markdown_templates"
DefaultGHHostname = "github.com"
DefaultGiteaBaseURL = "https://gitea.com"
DefaultGiteaPageSize = 30
DefaultGitlabHostname = "gitlab.com"
DefaultLockingDBType = "boltdb"
DefaultLogLevel = "info"
Expand Down Expand Up @@ -318,6 +325,22 @@ var stringFlags = map[string]stringFlag{
"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.",
},
GiteaBaseURLFlag: {
description: "Base URL of Gitea server installation. Must include 'http://' or 'https://'.",
},
GiteaUserFlag: {
description: "Gitea username of API user.",
defaultValue: "",
},
GiteaTokenFlag: {
description: "Gitea token of API user. Can also be specified via the ATLANTIS_GITEA_TOKEN environment variable.",
},
GiteaWebhookSecretFlag: {
description: "Optional secret used to validate Gitea webhooks." +
" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Gitea. " +
"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " +
"Should be specified via the ATLANTIS_GITEA_WEBHOOK_SECRET environment variable.",
},
GitlabHostnameFlag: {
description: "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.",
defaultValue: DefaultGitlabHostname,
Expand Down Expand Up @@ -568,6 +591,10 @@ var intFlags = map[string]intFlag{
" If merge base is further behind than this number of commits from any of branches heads, full fetch will be performed.",
defaultValue: DefaultCheckoutDepth,
},
GiteaPageSizeFlag: {
description: "Optional value that specifies the number of results per page to expect from Gitea.",
defaultValue: DefaultGiteaPageSize,
},
ParallelPoolSize: {
description: "Max size of the wait group that runs parallel plans and applies (if enabled).",
defaultValue: DefaultParallelPoolSize,
Expand Down Expand Up @@ -813,6 +840,12 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.GitlabHostname == "" {
c.GitlabHostname = DefaultGitlabHostname
}
if c.GiteaBaseURL == "" {
c.GiteaBaseURL = DefaultGiteaBaseURL
}
if c.GiteaPageSize == 0 {
c.GiteaPageSize = DefaultGiteaPageSize
}
if c.BitbucketBaseURL == "" {
c.BitbucketBaseURL = DefaultBitbucketBaseURL
}
Expand Down Expand Up @@ -885,12 +918,17 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
// 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 == "")) {
// 3. gitea user and token set
// 4. gitlab user and token set
// 5. bitbucket user and token set
// 6. azuredevops user and token set
// 7. 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 or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GHAppIDFlag, GHAppKeyFileFlag, GHAppIDFlag, GHAppKeyFlag, GiteaUserFlag, GiteaTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag)
if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) ||
((userConfig.GiteaUser == "") != (userConfig.GiteaToken == "")) ||
((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) ||
((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) ||
((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "")) {
return vcsErr
}
if (userConfig.GithubAppID != 0) && ((userConfig.GithubAppKey == "") && (userConfig.GithubAppKeyFile == "")) {
Expand All @@ -901,7 +939,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
}
// 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 == "" {
if userConfig.GithubAppID == 0 && userConfig.GithubUser == "" && userConfig.GiteaUser == "" && userConfig.GitlabUser == "" && userConfig.BitbucketUser == "" && userConfig.AzureDevopsUser == "" {
return vcsErr
}

Expand All @@ -924,6 +962,14 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
return fmt.Errorf("--%s must have http:// or https://, got %q", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL)
}

parsed, err = url.Parse(userConfig.GiteaBaseURL)
if err != nil {
return fmt.Errorf("error parsing --%s flag value %q: %s", GiteaWebhookSecretFlag, userConfig.GiteaBaseURL, err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("--%s must have http:// or https://, got %q", GiteaBaseURLFlag, userConfig.GiteaBaseURL)
}

if userConfig.RepoConfig != "" && userConfig.RepoConfigJSON != "" {
return fmt.Errorf("cannot use --%s and --%s at the same time", RepoConfigFlag, RepoConfigJSONFlag)
}
Expand All @@ -936,6 +982,8 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
GitlabWebhookSecretFlag: userConfig.GitlabWebhookSecret,
BitbucketTokenFlag: userConfig.BitbucketToken,
BitbucketWebhookSecretFlag: userConfig.BitbucketWebhookSecret,
GiteaTokenFlag: userConfig.GiteaToken,
GiteaWebhookSecretFlag: userConfig.GiteaWebhookSecret,
} {
if strings.Contains(token, "\n") {
s.Logger.Warn("--%s contains a newline which is usually unintentional", name)
Expand Down Expand Up @@ -1029,6 +1077,7 @@ func (s *ServerCmd) setVarFileAllowlist(userConfig *server.UserConfig) {
// 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.GiteaUser = strings.TrimPrefix(userConfig.GiteaUser, "@")
userConfig.GitlabUser = strings.TrimPrefix(userConfig.GitlabUser, "@")
userConfig.BitbucketUser = strings.TrimPrefix(userConfig.BitbucketUser, "@")
userConfig.AzureDevopsUser = strings.TrimPrefix(userConfig.AzureDevopsUser, "@")
Expand All @@ -1038,6 +1087,9 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) {
if userConfig.GithubUser != "" && userConfig.GithubWebhookSecret == "" && !s.SilenceOutput {
s.Logger.Warn("no GitHub webhook secret set. This could allow attackers to spoof requests from GitHub")
}
if userConfig.GiteaUser != "" && userConfig.GiteaWebhookSecret == "" && !s.SilenceOutput {
s.Logger.Warn("no Gitea webhook secret set. This could allow attackers to spoof requests from Gitea")
}
if userConfig.GitlabUser != "" && userConfig.GitlabWebhookSecret == "" && !s.SilenceOutput {
s.Logger.Warn("no GitLab webhook secret set. This could allow attackers to spoof requests from GitLab")
}
Expand Down
104 changes: 103 additions & 1 deletion cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ var testFlags = map[string]interface{}{
GHAppSlugFlag: "atlantis",
GHOrganizationFlag: "",
GHWebhookSecretFlag: "secret",
GiteaBaseURLFlag: "http://localhost",
GiteaTokenFlag: "gitea-token",
GiteaUserFlag: "gitea-user",
GiteaWebhookSecretFlag: "gitea-secret",
GiteaPageSizeFlag: 30,
GitlabHostnameFlag: "gitlab-hostname",
GitlabTokenFlag: "gitlab-token",
GitlabUserFlag: "gitlab-user",
Expand Down Expand Up @@ -156,6 +161,7 @@ func TestExecute_Defaults(t *testing.T) {
c := setup(map[string]interface{}{
GHUserFlag: "user",
GHTokenFlag: "token",
GiteaBaseURLFlag: "http://localhost",
RepoAllowlistFlag: "*",
}, t)
err := c.Execute()
Expand All @@ -174,6 +180,7 @@ func TestExecute_Defaults(t *testing.T) {
strExceptions := map[string]string{
GHUserFlag: "user",
GHTokenFlag: "token",
GiteaBaseURLFlag: "http://localhost",
DataDirFlag: dataDir,
MarkdownTemplateOverridesDirFlag: markdownTemplateOverridesDir,
AtlantisURLFlag: "http://" + hostname + ":4141",
Expand Down Expand Up @@ -422,7 +429,7 @@ 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"
expErr := "--gh-user/--gh-token or --gh-app-id/--gh-app-key-file or --gh-app-id/--gh-app-key or --gitea-user/--gitea-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set"
cases := []struct {
description string
flags map[string]interface{}
Expand All @@ -440,6 +447,13 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
},
true,
},
{
"just gitea token set",
map[string]interface{}{
GiteaTokenFlag: "token",
},
true,
},
{
"just gitlab token set",
map[string]interface{}{
Expand Down Expand Up @@ -468,6 +482,13 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
},
true,
},
{
"just gitea user set",
map[string]interface{}{
GiteaUserFlag: "user",
},
true,
},
{
"just github app set",
map[string]interface{}{
Expand Down Expand Up @@ -534,6 +555,22 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
},
true,
},
{
"github user and gitea token set",
map[string]interface{}{
GHUserFlag: "user",
GiteaTokenFlag: "token",
},
true,
},
{
"gitea user and github token set",
map[string]interface{}{
GiteaUserFlag: "user",
GHTokenFlag: "token",
},
true,
},
{
"github user and github token set and should be successful",
map[string]interface{}{
Expand All @@ -542,6 +579,14 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
},
false,
},
{
"gitea user and gitea token set and should be successful",
map[string]interface{}{
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
},
false,
},
{
"github app and key file set and should be successful",
map[string]interface{}{
Expand Down Expand Up @@ -587,6 +632,8 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
map[string]interface{}{
GHUserFlag: "user",
GHTokenFlag: "token",
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
GitlabUserFlag: "user",
GitlabTokenFlag: "token",
BitbucketUserFlag: "user",
Expand Down Expand Up @@ -699,6 +746,19 @@ func TestExecute_GithubApp(t *testing.T) {
Equals(t, int64(1), passedConfig.GithubAppID)
}

func TestExecute_GiteaUser(t *testing.T) {
t.Log("Should remove the @ from the gitea username if it's passed.")
c := setup(map[string]interface{}{
GiteaUserFlag: "@user",
GiteaTokenFlag: "token",
RepoAllowlistFlag: "*",
}, t)
err := c.Execute()
Ok(t, err)

Equals(t, "user", passedConfig.GiteaUser)
}

func TestExecute_GitlabUser(t *testing.T) {
t.Log("Should remove the @ from the gitlab username if it's passed.")
c := setup(map[string]interface{}{
Expand Down Expand Up @@ -934,3 +994,45 @@ func configVal(t *testing.T, u server.UserConfig, tag string) interface{} {
t.Fatalf("no field with tag %q found", tag)
return nil
}

// Gitea base URL must have a scheme.
func TestExecute_GiteaBaseURLScheme(t *testing.T) {
c := setup(map[string]interface{}{
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
RepoAllowlistFlag: "*",
GiteaBaseURLFlag: "mydomain.com",
}, t)
ErrEquals(t, "--gitea-base-url must have http:// or https://, got \"mydomain.com\"", c.Execute())

c = setup(map[string]interface{}{
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
RepoAllowlistFlag: "*",
GiteaBaseURLFlag: "://mydomain.com",
}, t)
ErrEquals(t, "error parsing --gitea-webhook-secret flag value \"://mydomain.com\": parse \"://mydomain.com\": missing protocol scheme", c.Execute())
}

func TestExecute_GiteaWithWebhookSecret(t *testing.T) {
c := setup(map[string]interface{}{
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
RepoAllowlistFlag: "*",
GiteaWebhookSecretFlag: "my secret",
}, t)
err := c.Execute()
Ok(t, err)
}

// Port should be retained on base url.
func TestExecute_GiteaBaseURLPort(t *testing.T) {
c := setup(map[string]interface{}{
GiteaUserFlag: "user",
GiteaTokenFlag: "token",
RepoAllowlistFlag: "*",
GiteaBaseURLFlag: "http://mydomain.com:7990",
}, t)
Ok(t, c.Execute())
Equals(t, "http://mydomain.com:7990", passedConfig.GiteaBaseURL)
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/runatlantis/atlantis
go 1.22.1

require (
code.gitea.io/sdk/gitea v0.17.1
github.com/Masterminds/sprig/v3 v3.2.3
github.com/alicebob/miniredis/v2 v2.32.1
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0
Expand Down Expand Up @@ -77,10 +78,12 @@ require (
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand All @@ -97,7 +100,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
Expand Down
Loading

0 comments on commit 722c4a9

Please sign in to comment.