mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
Signed-off-by: pbhatnagar-oss <pbhatifiwork@gmail.com> Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
@@ -243,9 +243,9 @@ func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alph
|
|||||||
}
|
}
|
||||||
|
|
||||||
if g.enableGitHubAPIMetrics {
|
if g.enableGitHubAPIMetrics {
|
||||||
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
|
return pullrequest.NewGithubAppService(ctx, *auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
|
||||||
}
|
}
|
||||||
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
|
return pullrequest.NewGithubAppService(ctx, *auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
// always default to token, even if not set (public access)
|
// always default to token, even if not set (public access)
|
||||||
|
|||||||
@@ -296,9 +296,9 @@ func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argop
|
|||||||
}
|
}
|
||||||
|
|
||||||
if g.enableGitHubAPIMetrics {
|
if g.enableGitHubAPIMetrics {
|
||||||
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches, httpClient)
|
return scm_provider.NewGithubAppProviderFor(ctx, *auth, github.Organization, github.API, github.AllBranches, httpClient)
|
||||||
}
|
}
|
||||||
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches)
|
return scm_provider.NewGithubAppProviderFor(ctx, *auth, github.Organization, github.API, github.AllBranches)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
|
token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package github_app
|
package github_app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -8,40 +10,65 @@ import (
|
|||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
||||||
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
"github.com/argoproj/argo-cd/v3/util/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getOptionalHTTPClientAndTransport(optionalHTTPClient ...*http.Client) (*http.Client, http.RoundTripper) {
|
// getInstallationClient creates a new GitHub client with the specified installation ID.
|
||||||
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
|
// It also returns a ghinstallation.Transport, which can be used for git requests.
|
||||||
if len(optionalHTTPClient) > 0 && optionalHTTPClient[0] != nil && optionalHTTPClient[0].Transport != nil {
|
func getInstallationClient(g github_app_auth.Authentication, url string, httpClient ...*http.Client) (*github.Client, error) {
|
||||||
// will either use the provided custom httpClient and it's transport
|
if g.InstallationId <= 0 {
|
||||||
return httpClient, optionalHTTPClient[0].Transport
|
return nil, errors.New("installation ID is required for github")
|
||||||
}
|
}
|
||||||
// or the default httpClient and transport
|
|
||||||
return httpClient, http.DefaultTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client builds a github client for the given app authentication.
|
// Use provided HTTP client's transport or default
|
||||||
func Client(g github_app_auth.Authentication, url string, optionalHTTPClient ...*http.Client) (*github.Client, error) {
|
var transport http.RoundTripper
|
||||||
httpClient, transport := getOptionalHTTPClientAndTransport(optionalHTTPClient...)
|
if len(httpClient) > 0 && httpClient[0] != nil && httpClient[0].Transport != nil {
|
||||||
|
transport = httpClient[0].Transport
|
||||||
|
} else {
|
||||||
|
transport = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
rt, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
|
itr, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create github app install: %w", err)
|
return nil, fmt.Errorf("failed to create GitHub installation transport: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
url = g.EnterpriseBaseURL
|
url = g.EnterpriseBaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
var client *github.Client
|
var client *github.Client
|
||||||
httpClient.Transport = rt
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
client = github.NewClient(httpClient)
|
client = github.NewClient(&http.Client{Transport: itr})
|
||||||
} else {
|
return client, nil
|
||||||
rt.BaseURL = url
|
}
|
||||||
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
|
|
||||||
if err != nil {
|
itr.BaseURL = url
|
||||||
return nil, fmt.Errorf("failed to create github enterprise client: %w", err)
|
client, err = github.NewClient(&http.Client{Transport: itr}).WithEnterpriseURLs(url, url)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create GitHub enterprise client: %w", err)
|
||||||
}
|
}
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client builds a github client for the given app authentication.
|
||||||
|
func Client(ctx context.Context, g github_app_auth.Authentication, url, org string, optionalHTTPClient ...*http.Client) (*github.Client, error) {
|
||||||
|
if url == "" {
|
||||||
|
url = g.EnterpriseBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an installation ID is already provided, use it directly.
|
||||||
|
if g.InstallationId != 0 {
|
||||||
|
return getInstallationClient(g, url, optionalHTTPClient...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-discover installation ID using shared utility
|
||||||
|
// Pass optional HTTP client for metrics tracking
|
||||||
|
installationId, err := git.DiscoverGitHubAppInstallationID(ctx, g.Id, g.PrivateKey, url, org, optionalHTTPClient...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.InstallationId = installationId
|
||||||
|
return getInstallationClient(g, url, optionalHTTPClient...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package pull_request
|
package pull_request
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
||||||
@@ -8,9 +9,9 @@ import (
|
|||||||
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGithubAppService(g github_app_auth.Authentication, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) {
|
func NewGithubAppService(ctx context.Context, g github_app_auth.Authentication, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) {
|
||||||
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
|
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
|
||||||
client, err := github_app.Client(g, url, httpClient)
|
client, err := github_app.Client(ctx, g, url, owner, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package scm_provider
|
package scm_provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
|
||||||
@@ -8,9 +9,9 @@ import (
|
|||||||
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGithubAppProviderFor(g github_app_auth.Authentication, organization string, url string, allBranches bool, optionalHTTPClient ...*http.Client) (*GithubProvider, error) {
|
func NewGithubAppProviderFor(ctx context.Context, g github_app_auth.Authentication, organization string, url string, allBranches bool, optionalHTTPClient ...*http.Client) (*GithubProvider, error) {
|
||||||
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
|
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
|
||||||
client, err := github_app.Client(g, url, httpClient)
|
client, err := github_app.Client(ctx, g, url, organization, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ func NewGenRepoSpecCommand() *cobra.Command {
|
|||||||
|
|
||||||
# Add a private HTTP OCI repository named 'stable'
|
# Add a private HTTP OCI repository named 'stable'
|
||||||
argocd admin repo generate-spec oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
argocd admin repo generate-spec oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
||||||
|
|
||||||
|
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
|
argocd admin repo generate-spec https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
|
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
|
argocd admin repo generate-spec https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
|
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
||||||
|
argocd admin repo generate-spec https://source.developers.google.com/p/my-google-cloud-project/r/my-repo --gcp-service-account-key-path service-account-key.json
|
||||||
`
|
`
|
||||||
|
|
||||||
command := &cobra.Command{
|
command := &cobra.Command{
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|||||||
# Add a private HTTP OCI repository named 'stable'
|
# Add a private HTTP OCI repository named 'stable'
|
||||||
argocd repo add oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
argocd repo add oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
||||||
|
|
||||||
# Add a private Git repository on GitHub.com via GitHub App
|
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repo add https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
argocd repo add https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
# Add a private Git repository on GitHub Enterprise via GitHub App
|
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repo add https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
argocd repo add https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
|||||||
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
||||||
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||||
|
|
||||||
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos
|
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repocreds add https://github.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
argocd repocreds add https://github.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos
|
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repocreds add https://ghe.example.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
argocd repocreds add https://ghe.example.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
# Add credentials with helm oci registry so that these oci registry urls do not need to be added as repos individually.
|
# Add credentials with helm oci registry so that these oci registry urls do not need to be added as repos individually.
|
||||||
@@ -191,7 +191,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
|||||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key (must be PEM format)")
|
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key (must be PEM format)")
|
||||||
command.Flags().Int64Var(&repo.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
|
command.Flags().Int64Var(&repo.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
|
||||||
command.Flags().Int64Var(&repo.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application")
|
command.Flags().Int64Var(&repo.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application (optional, will be auto-discovered if not provided)")
|
||||||
command.Flags().StringVar(&githubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
|
command.Flags().StringVar(&githubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
|
||||||
command.Flags().StringVar(&repo.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
|
command.Flags().StringVar(&repo.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
|
||||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
|
|||||||
command.Flags().BoolVar(&opts.EnableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
|
command.Flags().BoolVar(&opts.EnableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
|
||||||
command.Flags().BoolVar(&opts.EnableOci, "enable-oci", false, "enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)")
|
command.Flags().BoolVar(&opts.EnableOci, "enable-oci", false, "enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)")
|
||||||
command.Flags().Int64Var(&opts.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
|
command.Flags().Int64Var(&opts.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
|
||||||
command.Flags().Int64Var(&opts.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application")
|
command.Flags().Int64Var(&opts.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application (optional, will be auto-discovered if not provided)")
|
||||||
command.Flags().StringVar(&opts.GithubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
|
command.Flags().StringVar(&opts.GithubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
|
||||||
command.Flags().StringVar(&opts.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
|
command.Flags().StringVar(&opts.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
|
||||||
command.Flags().StringVar(&opts.Proxy, "proxy", "", "use proxy to access repository")
|
command.Flags().StringVar(&opts.Proxy, "proxy", "", "use proxy to access repository")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func getCredentialType(repo *v1alpha1.Repository) string {
|
|||||||
if repo.SSHPrivateKey != "" {
|
if repo.SSHPrivateKey != "" {
|
||||||
return "ssh"
|
return "ssh"
|
||||||
}
|
}
|
||||||
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
|
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 { // Promoter MVP: remove github-app-installation-id check since it is no longer a required field
|
||||||
return "github-app"
|
return "github-app"
|
||||||
}
|
}
|
||||||
if repo.GCPServiceAccountKey != "" {
|
if repo.GCPServiceAccountKey != "" {
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ argocd admin repo generate-spec REPOURL [flags]
|
|||||||
# Add a private HTTP OCI repository named 'stable'
|
# Add a private HTTP OCI repository named 'stable'
|
||||||
argocd admin repo generate-spec oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
argocd admin repo generate-spec oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
||||||
|
|
||||||
|
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
|
argocd admin repo generate-spec https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
|
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
|
argocd admin repo generate-spec https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
|
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
||||||
|
argocd admin repo generate-spec https://source.developers.google.com/p/my-google-cloud-project/r/my-repo --gcp-service-account-key-path service-account-key.json
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
@@ -58,7 +67,7 @@ argocd admin repo generate-spec REPOURL [flags]
|
|||||||
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
||||||
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
||||||
--github-app-id int id of the GitHub Application
|
--github-app-id int id of the GitHub Application
|
||||||
--github-app-installation-id int installation id of the GitHub Application
|
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
|
||||||
--github-app-private-key-path string private key of the GitHub Application
|
--github-app-private-key-path string private key of the GitHub Application
|
||||||
-h, --help help for generate-spec
|
-h, --help help for generate-spec
|
||||||
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
|
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
|
||||||
|
|||||||
6
docs/user-guide/commands/argocd_repo_add.md
generated
6
docs/user-guide/commands/argocd_repo_add.md
generated
@@ -47,10 +47,10 @@ argocd repo add REPOURL [flags]
|
|||||||
# Add a private HTTP OCI repository named 'stable'
|
# Add a private HTTP OCI repository named 'stable'
|
||||||
argocd repo add oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
argocd repo add oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
|
||||||
|
|
||||||
# Add a private Git repository on GitHub.com via GitHub App
|
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repo add https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
argocd repo add https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
# Add a private Git repository on GitHub Enterprise via GitHub App
|
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repo add https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
argocd repo add https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
|
||||||
@@ -69,7 +69,7 @@ argocd repo add REPOURL [flags]
|
|||||||
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
||||||
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
||||||
--github-app-id int id of the GitHub Application
|
--github-app-id int id of the GitHub Application
|
||||||
--github-app-installation-id int installation id of the GitHub Application
|
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
|
||||||
--github-app-private-key-path string private key of the GitHub Application
|
--github-app-private-key-path string private key of the GitHub Application
|
||||||
-h, --help help for add
|
-h, --help help for add
|
||||||
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
|
--insecure-ignore-host-key disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)
|
||||||
|
|||||||
6
docs/user-guide/commands/argocd_repocreds_add.md
generated
6
docs/user-guide/commands/argocd_repocreds_add.md
generated
@@ -20,10 +20,10 @@ argocd repocreds add REPOURL [flags]
|
|||||||
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
||||||
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||||
|
|
||||||
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos
|
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repocreds add https://github.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
argocd repocreds add https://github.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
|
||||||
|
|
||||||
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos
|
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
|
||||||
argocd repocreds add https://ghe.example.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
argocd repocreds add https://ghe.example.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
|
||||||
|
|
||||||
# Add credentials with helm oci registry so that these oci registry urls do not need to be added as repos individually.
|
# Add credentials with helm oci registry so that these oci registry urls do not need to be added as repos individually.
|
||||||
@@ -43,7 +43,7 @@ argocd repocreds add REPOURL [flags]
|
|||||||
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
--gcp-service-account-key-path string service account key for the Google Cloud Platform
|
||||||
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
--github-app-enterprise-base-url string base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3
|
||||||
--github-app-id int id of the GitHub Application
|
--github-app-id int id of the GitHub Application
|
||||||
--github-app-installation-id int installation id of the GitHub Application
|
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
|
||||||
--github-app-private-key-path string private key of the GitHub Application
|
--github-app-private-key-path string private key of the GitHub Application
|
||||||
-h, --help help for add
|
-h, --help help for add
|
||||||
--password string password to the repository
|
--password string password to the repository
|
||||||
|
|||||||
@@ -122,13 +122,16 @@ argocd repo add https://github.com/argoproj/argocd-example-apps.git --github-app
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> To add a private Git repository on GitHub Enterprise using the CLI add `--github-app-enterprise-base-url https://ghe.example.com/api/v3` flag.
|
> To add a private Git repository on GitHub Enterprise using the CLI add `--github-app-enterprise-base-url https://ghe.example.com/api/v3` flag.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The `--github-app-installation-id` flag is optional. If omitted, Argo CD will automatically discover the installation ID based on the repository's organization.
|
||||||
|
|
||||||
Using the UI:
|
Using the UI:
|
||||||
|
|
||||||
1. Navigate to `Settings/Repositories`
|
1. Navigate to `Settings/Repositories`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
2. Click `Connect Repo using GitHub App` button, choose type: `GitHub` or `GitHub Enterprise`, enter the URL, App Id, Installation Id, and the app's private key.
|
2. Click `Connect Repo using GitHub App` button, choose type: `GitHub` or `GitHub Enterprise`, enter the URL, App Id, Installation Id (optional), and the app's private key.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Enter the GitHub Enterprise Base URL for type `GitHub Enterprise`.
|
> Enter the GitHub Enterprise Base URL for type `GitHub Enterprise`.
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ stringData:
|
|||||||
url: "https://github.com/<your org or user>/<your repo>"
|
url: "https://github.com/<your org or user>/<your repo>"
|
||||||
type: "git"
|
type: "git"
|
||||||
githubAppID: "<your app ID here>"
|
githubAppID: "<your app ID here>"
|
||||||
githubAppInstallationID: "<your installation ID here>"
|
# githubAppInstallationID is optional and will be auto-discovered if omitted
|
||||||
|
githubAppInstallationID: "<your installation ID here>" # Optional
|
||||||
githubAppPrivateKey: |
|
githubAppPrivateKey: |
|
||||||
<your private key here>
|
<your private key here>
|
||||||
---
|
---
|
||||||
@@ -78,7 +79,8 @@ stringData:
|
|||||||
url: "https://github.com/<your org or user>/<your repo>"
|
url: "https://github.com/<your org or user>/<your repo>"
|
||||||
type: "git"
|
type: "git"
|
||||||
githubAppID: "<your app ID here>"
|
githubAppID: "<your app ID here>"
|
||||||
githubAppInstallationID: "<your installation ID here>"
|
# githubAppInstallationID is optional and will be auto-discovered if omitted
|
||||||
|
githubAppInstallationID: "<your installation ID here>" # Optional
|
||||||
githubAppPrivateKey: |
|
githubAppPrivateKey: |
|
||||||
<your private key here>
|
<your private key here>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/util/oci"
|
"github.com/argoproj/argo-cd/v3/util/oci"
|
||||||
|
|
||||||
@@ -239,8 +241,33 @@ func (repo *Repository) GetGitCreds(store git.CredsStore) git.Creds {
|
|||||||
if repo.SSHPrivateKey != "" {
|
if repo.SSHPrivateKey != "" {
|
||||||
return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), repo.Proxy)
|
return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), repo.Proxy)
|
||||||
}
|
}
|
||||||
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
|
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 { // Promoter MVP: remove github-app-installation-id check since it is no longer a required field
|
||||||
return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, repo.NoProxy, store)
|
installationId := repo.GithubAppInstallationId
|
||||||
|
|
||||||
|
// Auto-discover installation ID if not provided
|
||||||
|
if installationId == 0 {
|
||||||
|
org, err := git.ExtractOrgFromRepoURL(repo.Repo)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to extract organization from repository URL %s for GitHub App auto-discovery: %v", repo.Repo, err)
|
||||||
|
return git.NopCreds{}
|
||||||
|
}
|
||||||
|
if org != "" {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
discoveredId, err := git.DiscoverGitHubAppInstallationID(ctx, repo.GithubAppId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, org)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to auto-discover GitHub App installation ID for org %s: %v. Proceeding with installation ID 0.", org, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("Auto-discovered GitHub App installation ID %d for org %s", discoveredId, org)
|
||||||
|
installationId = discoveredId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warnf("Could not extract organization from repository URL %s for GitHub App auto-discovery", repo.Repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return git.NewGitHubAppCreds(repo.GithubAppId, installationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, repo.NoProxy, store)
|
||||||
}
|
}
|
||||||
if repo.GCPServiceAccountKey != "" {
|
if repo.GCPServiceAccountKey != "" {
|
||||||
return git.NewGoogleCloudCreds(repo.GCPServiceAccountKey, store)
|
return git.NewGoogleCloudCreds(repo.GCPServiceAccountKey, store)
|
||||||
|
|||||||
@@ -217,7 +217,6 @@ export const ReposList = ({match, location}: RouteComponentProps) => {
|
|||||||
return {
|
return {
|
||||||
url: (!githubAppValues.url && 'Repository URL is required') || (credsTemplate && !isHTTPOrHTTPSUrl(githubAppValues.url) && 'Not a valid HTTP/HTTPS URL'),
|
url: (!githubAppValues.url && 'Repository URL is required') || (credsTemplate && !isHTTPOrHTTPSUrl(githubAppValues.url) && 'Not a valid HTTP/HTTPS URL'),
|
||||||
githubAppId: !githubAppValues.githubAppId && 'GitHub App ID is required',
|
githubAppId: !githubAppValues.githubAppId && 'GitHub App ID is required',
|
||||||
githubAppInstallationId: !githubAppValues.githubAppInstallationId && 'GitHub App installation ID is required',
|
|
||||||
githubAppPrivateKey: !githubAppValues.githubAppPrivateKey && 'GitHub App private Key is required'
|
githubAppPrivateKey: !githubAppValues.githubAppPrivateKey && 'GitHub App private Key is required'
|
||||||
};
|
};
|
||||||
case ConnectionMethod.GOOGLECLOUD:
|
case ConnectionMethod.GOOGLECLOUD:
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
giturls "github.com/chainguard-dev/git-urls"
|
||||||
"github.com/google/go-github/v69/github"
|
"github.com/google/go-github/v69/github"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -43,6 +46,10 @@ var (
|
|||||||
|
|
||||||
// In memory cache for storing Azure tokens
|
// In memory cache for storing Azure tokens
|
||||||
azureTokenCache *gocache.Cache
|
azureTokenCache *gocache.Cache
|
||||||
|
|
||||||
|
// installationIdCache caches installation IDs for organizations to avoid redundant API calls.
|
||||||
|
githubInstallationIdCache *gocache.Cache
|
||||||
|
githubInstallationIdCacheMutex sync.RWMutex // For bulk API call coordination
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -66,6 +73,7 @@ func init() {
|
|||||||
// oauth2.TokenSource handles fetching new Tokens once they are expired. The oauth2.TokenSource itself does not expire.
|
// oauth2.TokenSource handles fetching new Tokens once they are expired. The oauth2.TokenSource itself does not expire.
|
||||||
googleCloudTokenSource = gocache.New(gocache.NoExpiration, 0)
|
googleCloudTokenSource = gocache.New(gocache.NoExpiration, 0)
|
||||||
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
|
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
|
||||||
|
githubInstallationIdCache = gocache.New(60*time.Minute, 60*time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoopCredsStore struct{}
|
type NoopCredsStore struct{}
|
||||||
@@ -576,6 +584,199 @@ func (g GitHubAppCreds) GetClientCertKey() string {
|
|||||||
return g.clientCertKey
|
return g.clientCertKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitHub App installation discovery cache and helper
|
||||||
|
|
||||||
|
// DiscoverGitHubAppInstallationID discovers the GitHub App installation ID for a given organization.
|
||||||
|
// It queries the GitHub API to list all installations for the app and returns the installation ID
|
||||||
|
// for the matching organization. Results are cached to avoid redundant API calls.
|
||||||
|
// An optional HTTP client can be provided for custom transport (e.g., for metrics tracking).
|
||||||
|
func DiscoverGitHubAppInstallationID(ctx context.Context, appId int64, privateKey, enterpriseBaseURL, org string, httpClient ...*http.Client) (int64, error) {
|
||||||
|
domain, err := domainFromBaseURL(enterpriseBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get domain from base URL: %w", err)
|
||||||
|
}
|
||||||
|
org = strings.ToLower(org)
|
||||||
|
// Check cache first
|
||||||
|
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower(org), domain, appId)
|
||||||
|
if id, found := githubInstallationIdCache.Get(cacheKey); found {
|
||||||
|
return id.(int64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided HTTP client or default
|
||||||
|
var transport http.RoundTripper
|
||||||
|
if len(httpClient) > 0 && httpClient[0] != nil && httpClient[0].Transport != nil {
|
||||||
|
transport = httpClient[0].Transport
|
||||||
|
} else {
|
||||||
|
transport = http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GitHub App transport
|
||||||
|
rt, err := ghinstallation.NewAppsTransport(transport, appId, []byte(privateKey))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to create GitHub app transport: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enterpriseBaseURL != "" {
|
||||||
|
rt.BaseURL = enterpriseBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GitHub client
|
||||||
|
var client *github.Client
|
||||||
|
clientTransport := &http.Client{Transport: rt}
|
||||||
|
if enterpriseBaseURL == "" {
|
||||||
|
client = github.NewClient(clientTransport)
|
||||||
|
} else {
|
||||||
|
client, err = github.NewClient(clientTransport).WithEnterpriseURLs(enterpriseBaseURL, enterpriseBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to create GitHub enterprise client: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all installations and cache them
|
||||||
|
var allInstallations []*github.Installation
|
||||||
|
opts := &github.ListOptions{PerPage: 100}
|
||||||
|
|
||||||
|
// Lock for the entire loop to avoid multiple concurrent API calls on startup
|
||||||
|
githubInstallationIdCacheMutex.Lock()
|
||||||
|
defer githubInstallationIdCacheMutex.Unlock()
|
||||||
|
|
||||||
|
// Check cache again inside the write lock in case another goroutine already fetched it
|
||||||
|
if id, found := githubInstallationIdCache.Get(cacheKey); found {
|
||||||
|
return id.(int64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
installations, resp, err := client.Apps.ListInstallations(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to list installations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allInstallations = append(allInstallations, installations...)
|
||||||
|
|
||||||
|
if resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
opts.Page = resp.NextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache all installation IDs
|
||||||
|
for _, installation := range allInstallations {
|
||||||
|
if installation.Account != nil && installation.Account.Login != nil && installation.ID != nil {
|
||||||
|
githubInstallationIdCache.Set(cacheKey, *installation.ID, gocache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the installation ID for the requested org
|
||||||
|
if id, found := githubInstallationIdCache.Get(cacheKey); found {
|
||||||
|
return id.(int64), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("installation not found for org: %s", org)
|
||||||
|
}
|
||||||
|
|
||||||
|
// domainFromBaseURL extracts the host (domain) from the given GitHub base URL.
|
||||||
|
// Supports HTTP(S), SSH URLs, and git@host:org/repo forms.
|
||||||
|
// Returns an error if a domain cannot be extracted.
|
||||||
|
func domainFromBaseURL(baseURL string) (string, error) {
|
||||||
|
if baseURL == "" {
|
||||||
|
return "github.com", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 1. SSH-style Git URL: git@github.com:org/repo.git ---
|
||||||
|
if strings.Contains(baseURL, "@") && strings.Contains(baseURL, ":") && !strings.Contains(baseURL, "://") {
|
||||||
|
parts := strings.SplitN(baseURL, "@", 2)
|
||||||
|
right := parts[len(parts)-1] // github.com:org/repo
|
||||||
|
host := strings.SplitN(right, ":", 2)[0] // github.com
|
||||||
|
if host != "" {
|
||||||
|
return host, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed to extract host from SSH-style URL: %q", baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Ensure scheme so url.Parse works ---
|
||||||
|
if !strings.HasPrefix(baseURL, "http://") &&
|
||||||
|
!strings.HasPrefix(baseURL, "https://") &&
|
||||||
|
!strings.HasPrefix(baseURL, "ssh://") {
|
||||||
|
baseURL = "https://" + baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. Standard URL parse ---
|
||||||
|
parsed, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse URL %q: %w", baseURL, err)
|
||||||
|
}
|
||||||
|
if parsed.Host == "" {
|
||||||
|
return "", fmt.Errorf("URL %q parsed but host is empty", baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
host := parsed.Host
|
||||||
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
return host, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractOrgFromRepoURL extracts the organization/owner name from a GitHub repository URL.
|
||||||
|
// Supports formats:
|
||||||
|
// - HTTPS: https://github.com/org/repo.git
|
||||||
|
// - SSH: git@github.com:org/repo.git
|
||||||
|
// - SSH with port: git@github.com:22/org/repo.git or ssh://git@github.com:22/org/repo.git
|
||||||
|
func ExtractOrgFromRepoURL(repoURL string) (string, error) {
|
||||||
|
if repoURL == "" {
|
||||||
|
return "", errors.New("repo URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle edge case: ssh://git@host:org/repo (malformed but used in practice)
|
||||||
|
// This format mixes ssh:// prefix with colon notation instead of using a slash.
|
||||||
|
// Convert it to git@host:org/repo which git-urls can parse correctly.
|
||||||
|
// We distinguish this from the valid ssh://git@host:22/org/repo (with port number).
|
||||||
|
if strings.HasPrefix(repoURL, "ssh://git@") {
|
||||||
|
remainder := strings.TrimPrefix(repoURL, "ssh://")
|
||||||
|
if colonIdx := strings.Index(remainder, ":"); colonIdx != -1 {
|
||||||
|
afterColon := remainder[colonIdx+1:]
|
||||||
|
slashIdx := strings.Index(afterColon, "/")
|
||||||
|
|
||||||
|
// Check if what follows the colon is a port number
|
||||||
|
isPort := false
|
||||||
|
if slashIdx > 0 {
|
||||||
|
if _, err := strconv.Atoi(afterColon[:slashIdx]); err == nil {
|
||||||
|
isPort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a port, it's the malformed format - strip ssh:// prefix
|
||||||
|
if !isPort && slashIdx != 0 {
|
||||||
|
repoURL = remainder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use git-urls library to parse all Git URL formats
|
||||||
|
parsed, err := giturls.Parse(repoURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse repository URL %q: %w", repoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the path: remove leading/trailing slashes and .git suffix
|
||||||
|
path := strings.Trim(parsed.Path, "/")
|
||||||
|
path = strings.TrimSuffix(path, ".git")
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return "", fmt.Errorf("repository URL %q does not contain a path", repoURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the first path component (organization/owner)
|
||||||
|
// Path format is typically "org/repo" or "org/repo/subpath"
|
||||||
|
if idx := strings.Index(path, "/"); idx > 0 {
|
||||||
|
org := path[:idx]
|
||||||
|
// Normalize to lowercase for case-insensitive comparison
|
||||||
|
return strings.ToLower(org), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no slash, the entire path might be just the org (unusual but handle it)
|
||||||
|
// This would fail validation later, but let's return it
|
||||||
|
return "", fmt.Errorf("could not extract organization from repository URL %q: path %q does not contain org/repo format", repoURL, path)
|
||||||
|
}
|
||||||
|
|
||||||
var _ Creds = GoogleCloudCreds{}
|
var _ Creds = GoogleCloudCreds{}
|
||||||
|
|
||||||
// GoogleCloudCreds to authenticate to Google Cloud Source repositories
|
// GoogleCloudCreds to authenticate to Google Cloud Source repositories
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -515,3 +519,132 @@ func TestAzureWorkloadIdentityCreds_ReuseTokenIfExistingIsNotExpired(t *testing.
|
|||||||
func resetAzureTokenCache() {
|
func resetAzureTokenCache() {
|
||||||
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
|
azureTokenCache = gocache.New(gocache.NoExpiration, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fakeGitHubAppPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEA2KHkp2fe0ReHqAt9BimWEec2ryWZIyg9jvB3BdP3mzFf0bOt
|
||||||
|
WlHm1FAETFxH4h5jYASUaaWEwRNNyGlT1GhTp+jOMC4xhOSb5/SnI2dt2EITkudQ
|
||||||
|
FKsSFUdJAndqOzkjrP2pL4fi4b7JWhuLDO36ufAP4l2m3tnAseGSSTIccWvzLFFU
|
||||||
|
s3wsHOHxOcJGCP1Z7rizxl6mTKYL/Z+GHqN17OJslDf901uPXsUeDCYL2iigGPhD
|
||||||
|
Ao6k8POsfbpqLG7poCDTK50FLnS5qEocjxt+J4ZjBEWTU/DOFXWYstzfbhm8OZPQ
|
||||||
|
pSSEiBCxpg+zjtkQfCyZxXB5RQ84CY78fXOI9QIDAQABAoIBAG8jL0FLIp62qZvm
|
||||||
|
uO9ualUo/37/lP7aaCpq50UQJ9lwjS3yNh8+IWQO4QWj2iUBXg4mi1Vf2ymKk78b
|
||||||
|
eixgkXp1D0Lcj/8ToYBwnUami04FKDGXhhf0Y8SS27vuM4vKlqjrQd7modkangYi
|
||||||
|
V0X82UKHDD8fuLpfkGIxzXDLypfMzjMuVpSntnWaf2YX3VR/0/66yEp9GejftF2k
|
||||||
|
wqhGoWM6r68pN5XuCqWd5PRluSoDy/o4BAFMhYCSfp9PjgZE8aoeWHgYzlZ3gUyn
|
||||||
|
r+HaDDNWbibhobXk/9h8lwAJ6KCZ5RZ+HFfh0HuwIxmocT9OCFgy/S0g1p+o3m9K
|
||||||
|
VNd5AMkCgYEA5fbS5UK7FBzuLoLgr1hktmbLJhpt8y8IPHNABHcUdE+O4/1xTQNf
|
||||||
|
pMUwkKjGG1MtrGjLOIoMGURKKn8lR1GMZueOTSKY0+mAWUGvSzl6vwtJwvJruT8M
|
||||||
|
otEO03o0tPnRKGxbFjqxkp2b6iqJ8MxCRZ3lSidc4mdi7PHzv9lwgvsCgYEA8Siq
|
||||||
|
7weCri9N6y+tIdORAXgRzcW54BmJyqB147c72RvbMacb6rN28KXpM3qnRXyp3Llb
|
||||||
|
yh81TW3FH10GqrjATws7BK8lP9kkAw0Z/7kNiS1NgH3pUbO+5H2kAa/6QW35nzRe
|
||||||
|
Jw2lyfYGWqYO4hYXH14ML1kjgS1hgd3XHOQ64M8CgYAKcjDYSzS2UC4dnMJaFLjW
|
||||||
|
dErsGy09a7iDDnUs/r/GHMsP3jZkWi/hCzgOiiwdl6SufUAl/FdaWnjH/2iRGco3
|
||||||
|
7nLPXC/3CFdVNp+g2iaSQRADtAFis9N+HeL/hkCYq/RtUqa8lsP0NgacF3yWnKCy
|
||||||
|
Ct8chDc67ZlXzBHXeCgdOwKBgHHGFPbWXUHeUW1+vbiyvrupsQSanznp8oclMtkv
|
||||||
|
Dk48hSokw9fzuU6Jh77gw9/Vk7HtxS9Tj+squZA1bDrJFPl1u+9WzkUUJZhG6xgp
|
||||||
|
bwhj1iejv5rrKUlVOTYOlwudXeJNa4oTNz9UEeVcaLMjZt9GmIsSC90a0uDZD26z
|
||||||
|
AlAjAoGAEoqm2DcNN7SrH6aVFzj1EVOrNsHYiXj/yefspeiEmf27PSAslP+uF820
|
||||||
|
SDpz4h+Bov5qTKkzcxuu1QWtA4M0K8Iy6IYLwb83DZEm1OsAf4i0pODz21PY/I+O
|
||||||
|
VHzjB10oYgaInHZgMUdyb6F571UdiYSB6a/IlZ3ngj5touy3VIM=
|
||||||
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
|
func TestDiscoverGitHubAppInstallationID(t *testing.T) {
|
||||||
|
t.Run("returns cached installation ID", func(t *testing.T) {
|
||||||
|
// Setup: prepopulate cache
|
||||||
|
org := "test-org"
|
||||||
|
appId := int64(12345)
|
||||||
|
domain := "github.com"
|
||||||
|
expectedId := int64(98765)
|
||||||
|
|
||||||
|
// Clean up at both start and end to ensure test isolation
|
||||||
|
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower(org), domain, appId)
|
||||||
|
|
||||||
|
githubInstallationIdCache.Set(cacheKey, expectedId, gocache.NoExpiration)
|
||||||
|
|
||||||
|
// Ensure cleanup even if test fails
|
||||||
|
t.Cleanup(func() {
|
||||||
|
githubInstallationIdCache.Delete(cacheKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
ctx := context.Background()
|
||||||
|
actualId, err := DiscoverGitHubAppInstallationID(ctx, appId, "fake-key", "", org)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedId, actualId)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("discovers installation ID from GitHub API", func(t *testing.T) {
|
||||||
|
// Setup: mock GitHub API server
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// GitHub Enterprise expects paths like /api/v3/app/installations
|
||||||
|
// go-github's WithEnterpriseURLs adds this prefix automatically
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/app/installations") {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
//nolint:errcheck
|
||||||
|
json.NewEncoder(w).Encode([]map[string]any{
|
||||||
|
{"id": 98765, "account": map[string]any{"login": "test-org"}},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Return 404 for any other path
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Clean up cache entry for this test on completion
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// Extract domain from server URL for proper cache key
|
||||||
|
domain, _ := domainFromBaseURL(server.URL)
|
||||||
|
cacheKey := fmt.Sprintf("%s:%s:%d", strings.ToLower("test-org"), domain, 12345)
|
||||||
|
githubInstallationIdCache.Delete(cacheKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute & Assert
|
||||||
|
ctx := context.Background()
|
||||||
|
// Pass the mock server URL as the enterpriseBaseURL so the GitHub client uses it
|
||||||
|
// Note: The mock server will have a different domain (e.g., 127.0.0.1) than the first test (github.com),
|
||||||
|
// so there's no cache collision between the two subtests.
|
||||||
|
actualId, err := DiscoverGitHubAppInstallationID(ctx, 12345, fakeGitHubAppPrivateKey, server.URL, "test-org")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(98765), actualId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractOrgFromRepoURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
repoURL string
|
||||||
|
expected string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"HTTPS URL", "https://github.com/argoproj/argo-cd", "argoproj", false},
|
||||||
|
{"HTTPS URL with .git", "https://github.com/argoproj/argo-cd.git", "argoproj", false},
|
||||||
|
{"HTTPS URL with port", "https://github.com:443/argoproj/argo-cd.git", "argoproj", false},
|
||||||
|
{"SSH URL", "git@github.com:argoproj/argo-cd.git", "argoproj", false},
|
||||||
|
{"SSH URL without .git", "git@github.com:argoproj/argo-cd", "argoproj", false},
|
||||||
|
{"SSH URL with ssh:// prefix", "ssh://git@github.com:argoproj/argo-cd.git", "argoproj", false},
|
||||||
|
{"SSH URL with port", "ssh://git@github.com:22/argoproj/argo-cd.git", "argoproj", false},
|
||||||
|
{"GitHub Enterprise HTTPS", "https://github.example.com/myorg/myrepo.git", "myorg", false},
|
||||||
|
{"GitHub Enterprise SSH", "git@github.example.com:myorg/myrepo.git", "myorg", false},
|
||||||
|
{"Case insensitive", "https://github.com/ArgoPROJ/argo-cd", "argoproj", false}, // Test case sensitivity
|
||||||
|
{"Invalid URL", "not-a-url", "", true},
|
||||||
|
{"Empty string", "", "", true},
|
||||||
|
{"URL without org/repo", "https://github.com", "", true},
|
||||||
|
{"URL with only org", "https://github.com/argoproj", "", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual, err := ExtractOrgFromRepoURL(tt.repoURL)
|
||||||
|
if tt.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Empty(t, actual)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/argoproj/argo-cd/v3/util/proxy"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/common"
|
"github.com/argoproj/argo-cd/v3/common"
|
||||||
"github.com/argoproj/argo-cd/v3/test/fixture/log"
|
"github.com/argoproj/argo-cd/v3/test/fixture/log"
|
||||||
"github.com/argoproj/argo-cd/v3/test/fixture/path"
|
"github.com/argoproj/argo-cd/v3/test/fixture/path"
|
||||||
"github.com/argoproj/argo-cd/v3/test/fixture/test"
|
"github.com/argoproj/argo-cd/v3/test/fixture/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Ensure tests use non-cached proxy callback
|
||||||
|
proxy.UseTestingProxyCallback()
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsCommitSHA(t *testing.T) {
|
func TestIsCommitSHA(t *testing.T) {
|
||||||
assert.True(t, IsCommitSHA("9d921f65f3c5373b682e2eb4b37afba6592e8f8b"))
|
assert.True(t, IsCommitSHA("9d921f65f3c5373b682e2eb4b37afba6592e8f8b"))
|
||||||
assert.True(t, IsCommitSHA("9D921F65F3C5373B682E2EB4B37AFBA6592E8F8B"))
|
assert.True(t, IsCommitSHA("9D921F65F3C5373B682E2EB4B37AFBA6592E8F8B"))
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ import (
|
|||||||
"golang.org/x/net/http/httpproxy"
|
"golang.org/x/net/http/httpproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultProxyCallback is the default proxy callback function that reads from environment variables. http.ProxyFromEnvironment
|
||||||
|
// is cached on first call, so we can't use it for tests. When writing a test that uses t.Setenv for some proxy env var,
|
||||||
|
// call UseTestingProxyCallback.
|
||||||
|
var DefaultProxyCallback = http.ProxyFromEnvironment
|
||||||
|
|
||||||
|
// UseTestingProxyCallback sets the DefaultProxyCallback to use httpproxy.FromEnvironment. This is useful for tests that
|
||||||
|
// use t.Setenv to set proxy env variables.
|
||||||
|
func UseTestingProxyCallback() {
|
||||||
|
DefaultProxyCallback = func(r *http.Request) (*url.URL, error) {
|
||||||
|
return httpproxy.FromEnvironment().ProxyFunc()(r.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UpsertEnv removes the existing proxy env variables and adds the custom proxy variables
|
// UpsertEnv removes the existing proxy env variables and adds the custom proxy variables
|
||||||
func UpsertEnv(cmd *exec.Cmd, proxy string, noProxy string) []string {
|
func UpsertEnv(cmd *exec.Cmd, proxy string, noProxy string) []string {
|
||||||
envs := []string{}
|
envs := []string{}
|
||||||
@@ -42,7 +55,7 @@ func GetCallback(proxy string, noProxy string) func(*http.Request) (*url.URL, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// read proxy from env variable if custom proxy is missing
|
// read proxy from env variable if custom proxy is missing
|
||||||
return http.ProxyFromEnvironment
|
return DefaultProxyCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpProxy(url string) string {
|
func httpProxy(url string) string {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -11,6 +12,13 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Ensure tests use non-cached proxy callback
|
||||||
|
UseTestingProxyCallback()
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
func TestAddProxyEnvIfAbsent(t *testing.T) {
|
func TestAddProxyEnvIfAbsent(t *testing.T) {
|
||||||
t.Run("Existing proxy env variables", func(t *testing.T) {
|
t.Run("Existing proxy env variables", func(t *testing.T) {
|
||||||
proxy := "https://proxy:5000"
|
proxy := "https://proxy:5000"
|
||||||
|
|||||||
Reference in New Issue
Block a user