Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2941102
feat(appset): add SCM-Manager support
tidiegeler Nov 21, 2024
b0cc040
add webhooks
tidiegeler Nov 21, 2024
30977b0
another fix
tidiegeler Nov 21, 2024
486a82c
Merge branch 'argoproj:master' into feature/scm_manager_support
tidiegeler Nov 21, 2024
1dca723
apply test fixes for SCM-Manager merge
tidiegeler Nov 22, 2024
0a2beda
Merge branch 'argoproj:master' into feature/scm_manager_support
tidiegeler Nov 25, 2024
fd52f69
Merge branch 'argoproj:master' into feature/scm_manager_support
tidiegeler Nov 28, 2024
e4a2fad
Replace HTMLUrl in SCMM with SourceUrl
tidiegeler Nov 28, 2024
642d642
Add screenshot for webhook configuration in SCM-Manager
pfeuffer Nov 28, 2024
5522238
Implement pull request hooks
pfeuffer Dec 3, 2024
84ddbed
Fix tests for util/webhook
pfeuffer Dec 4, 2024
45426dc
Fix tests for util/webhook
pfeuffer Dec 4, 2024
25eba06
Add test for applicationset/webhook
pfeuffer Dec 4, 2024
ad51074
Remove debug log
pfeuffer Dec 4, 2024
ec74aed
Bump goscm version
pfeuffer Dec 5, 2024
d483457
Merge remote-tracking branch 'origin/master' into feature/scm_manager…
pfeuffer Dec 5, 2024
00202c5
Fix import order
pfeuffer Dec 5, 2024
2dd4620
Merge remote-tracking branch 'origin/master' into feature/scm_manager…
pfeuffer Dec 5, 2024
b003349
Add documentation for SCM-Manager webhook configuration
pfeuffer Dec 6, 2024
ab23c4e
implement minor fixes
tidiegeler Dec 9, 2024
f4eb396
Merge remote-tracking branch 'origin/master' into feature/scm_manager…
pfeuffer Dec 9, 2024
b3de71b
Merge branch 'master' into feature/scm_manager_support
pfeuffer Jan 7, 2025
2915145
Fix linting errors
pfeuffer Jan 7, 2025
0444e4b
Merge branch master into feature/scm_manager_support
pfeuffer Sep 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions applicationset/generators/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
}
return pullrequest.NewAzureDevOpsService(token, providerConfig.API, providerConfig.Organization, providerConfig.Project, providerConfig.Repo, providerConfig.Labels)
}
if generatorConfig.ScmManager != nil {
providerConfig := generatorConfig.ScmManager
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}

var caCerts []byte
var prErr error
if providerConfig.CARef != nil {
caCerts, prErr = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if prErr != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", prErr)
}
}

return pullrequest.NewScmManagerService(ctx, token, providerConfig.API, providerConfig.Namespace, providerConfig.Name, providerConfig.Insecure, g.scmRootCAPath, caCerts)
}
return nil, errors.New("no Pull Request provider implementation configured")
}

Expand Down
19 changes: 19 additions & 0 deletions applicationset/generators/scm_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,25 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
if awsErr != nil {
return nil, fmt.Errorf("error initializing AWS codecommit service: %w", awsErr)
}
case providerConfig.ScmManager != nil:
providerConfig := providerConfig.ScmManager
var caCerts []byte
var scmError error
if providerConfig.CARef != nil {
caCerts, scmError = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if scmError != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", scmError)
}
}

token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if err != nil {
return nil, fmt.Errorf("error fetching SCM-Manager token: %w", err)
}
provider, err = scm_provider.NewScmManagerProvider(ctx, token, providerConfig.API, providerConfig.AllBranches, providerConfig.Insecure, g.scmRootCAPath, caCerts)
if err != nil {
return nil, fmt.Errorf("error initializing SCM-Manager provider: %w", err)
}
default:
return nil, errors.New("no SCM provider implementation configured")
}
Expand Down
68 changes: 68 additions & 0 deletions applicationset/services/pull_request/scm-manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package pull_request

import (
"context"
"net/http"
"os"
"strconv"

"github.com/argoproj/argo-cd/v2/applicationset/utils"

scmm "github.com/scm-manager/goscm"
)

type ScmManagerService struct {
client *scmm.Client
namespace string
name string
}

var _ PullRequestService = (*ScmManagerService)(nil)

func NewScmManagerService(ctx context.Context, token, url, namespace, name string, insecure bool, scmRootCAPath string, caCerts []byte) (PullRequestService, error) {
if token == "" {
token = os.Getenv("SCMM_TOKEN")
}

httpClient := &http.Client{}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
httpClient.Transport = tr

client, err := scmm.NewClient(url, token)
if err != nil {
return nil, err
}

client.SetHttpClient(httpClient)
return &ScmManagerService{
client: client,
namespace: namespace,
name: name,
}, nil
}

func (g *ScmManagerService) List(ctx context.Context) ([]*PullRequest, error) {
prs, err := g.client.ListPullRequests(g.namespace, g.name, g.client.NewPullRequestListFilter())
if err != nil {
return nil, err
}
list := []*PullRequest{}
for _, pr := range prs.Embedded.PullRequests {
changeset, err := g.client.GetHeadChangesetForBranch(g.namespace, g.name, pr.Source)
if err != nil {
return nil, err
}
prId, err := strconv.Atoi(pr.Id)
if err != nil {
return nil, err
}
list = append(list, &PullRequest{
Number: prId,
Branch: pr.Source,
HeadSHA: changeset.Id,
Labels: make([]string, 0),
})
}
return list, nil
}
110 changes: 110 additions & 0 deletions applicationset/services/pull_request/scm-manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package pull_request

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func scmmMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Println(r.RequestURI)
switch r.RequestURI {
case "/api/v2/pull-requests/test-argocd/pr-test?status=OPEN&pageSize=10":
_, err := io.WriteString(w, `{
"page": 0,
"pageTotal": 1,
"_embedded": {
"pullRequests": [
{
"id": "1",
"author": {
"id": "eheimbuch",
"displayName": "Eduard Heimbuch",
"mail": "[email protected]"
},
"reviser": {
"id": null,
"displayName": null
},
"closeDate": null,
"source": "test_pr",
"target": "main",
"title": "New feature xyz",
"description": "Awesome!",
"creationDate": "2023-01-23T12:58:56.770Z",
"lastModified": null,
"status": "OPEN",
"reviewer": [],
"tasks": {
"todo": 0,
"done": 0
},
"sourceRevision": null,
"targetRevision": null,
"markedAsReviewed": [],
"emergencyMerged": false,
"ignoredMergeObstacles": null
}
]
}
}`)
if err != nil {
t.Fail()
}
case "/api/v2/repositories/test-argocd/pr-test/branches/test_pr/changesets?&pageSize=1":
_, err := io.WriteString(w, `{
"page": 0,
"pageTotal": 1,
"_embedded": {
"changesets": [
{
"id": "b4ed814b1afe810c4902bc5590c7b09531296679",
"author": {
"mail": "[email protected]",
"name": "Eduard Heimbuch"
},
"date": "2023-07-03T08:53:15Z",
"description": "test url",
"contributors": [
{
"type": "Pushed-by",
"person": {
"mail": "[email protected]",
"name": "Eduard Heimbuch"
}
}
]
}
]
}
}`)
if err != nil {
t.Fail()
}
}
}
}

func TestScmManagerPrList(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scmmMockHandler(t)(w, r)
}))
defer ts.Close()
host, err := NewScmManagerService(context.Background(), "", ts.URL, "test-argocd", "pr-test", false, "", nil)
require.NoError(t, err)
prs, err := host.List(context.Background())
require.NoError(t, err)
assert.Len(t, prs, 1)
assert.Equal(t, 1, prs[0].Number)
assert.Equal(t, "test_pr", prs[0].Branch)
assert.Equal(t, "b4ed814b1afe810c4902bc5590c7b09531296679", prs[0].HeadSHA)
}
153 changes: 153 additions & 0 deletions applicationset/services/scm_provider/scm-manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package scm_provider

import (
"context"
"errors"
"fmt"
"net/http"
"os"

"github.com/argoproj/argo-cd/v2/applicationset/utils"

scmm "github.com/scm-manager/goscm"
)

type ScmManagerProvider struct {
client *scmm.Client
allBranches bool
}

const FilterLimit = 9999

var _ SCMProviderService = &ScmManagerProvider{}

func NewScmManagerProvider(ctx context.Context, token, url string, allBranches, insecure bool, scmRootCAPath string, caCerts []byte) (*ScmManagerProvider, error) {
if token == "" {
token = os.Getenv("SCMM_TOKEN")
}
httpClient := &http.Client{}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
httpClient.Transport = tr

client, err := scmm.NewClient(url, token)
if err != nil {
return nil, fmt.Errorf("error creating a new SCM-Manager client: %w", err)
}

client.SetHttpClient(httpClient)

return &ScmManagerProvider{
client: client,
allBranches: allBranches,
}, nil
}

func (g *ScmManagerProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
scmmRepo, err := g.client.GetRepo(repo.Organization, repo.Repository)
if err != nil {
return nil, err
}

if !g.allBranches {
defaultBranch, err := g.client.GetDefaultBranch(repo.Organization, repo.Repository)
if err != nil {
return nil, err
}

return []*Repository{
{
Organization: repo.Organization,
Repository: repo.Repository,
Branch: defaultBranch.Name,
URL: repo.URL,
SHA: defaultBranch.Revision,
Labels: make([]string, 0),
RepositoryId: scmmRepo.Namespace + "/" + scmmRepo.Name,
},
}, nil
}
repos := []*Repository{}
branches, err := g.client.ListRepoBranches(repo.Organization, repo.Repository)
if err != nil {
return nil, err
}
for _, branch := range branches.Embedded.Branches {
repos = append(repos, &Repository{
Organization: scmmRepo.Namespace,
Repository: scmmRepo.Name,
Branch: branch.Name,
URL: scmmRepo.Links.ProtocolUrl[0].Href,
SHA: branch.Revision,
Labels: make([]string, 0),
RepositoryId: scmmRepo.Namespace + "/" + scmmRepo.Name,
})
}
return repos, nil
}

func (g *ScmManagerProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
repos := []*Repository{}
filter := g.client.NewRepoListFilter()
filter.Limit = FilterLimit
scmmRepos, err := g.client.ListRepos(filter)
if err != nil {
return nil, err
}
for _, scmmRepo := range scmmRepos.Embedded.Repositories {
var url string
switch cloneProtocol {
// Default to SSH if unspecified (i.e. if ""). SSH Plugin needs to be installed
case "", "ssh":
url = getProtocolUrlByName(scmmRepo.Links.ProtocolUrl, "ssh")
case "https":
url = getProtocolUrlByName(scmmRepo.Links.ProtocolUrl, "http")
default:
return nil, fmt.Errorf("unknown clone protocol %v", cloneProtocol)
}

if url == "" {
return nil, errors.New("could not find valid repository protocol url")
}

defaultBranch, err := g.client.GetDefaultBranch(scmmRepo.Namespace, scmmRepo.Name)
if err != nil {
if errors.Is(err, scmm.ErrEmptyRepository) || errors.Is(err, scmm.ErrNoDefaultBranchFound) {
continue
}
return nil, err
}

repos = append(repos, &Repository{
Organization: scmmRepo.Namespace,
Repository: scmmRepo.Name,
Branch: defaultBranch.Name,
URL: url,
RepositoryId: scmmRepo.Namespace + "/" + scmmRepo.Name,
})
}
return repos, nil
}

func getProtocolUrlByName(urls []scmm.ProtocolUrl, name string) string {
for _, url := range urls {
if url.Name == name {
return url.Href
}
}
return ""
}

func (g *ScmManagerProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
_, resp, err := g.client.GetContent(repo.Organization, repo.Repository, repo.Branch, path)
if resp != nil && resp.StatusCode == http.StatusNotFound {
return false, nil
}
if err != nil {
if err.Error() == "expect file, got directory" {
return true, nil
}
return false, err
}
return true, nil
}
Loading
Loading