diff --git a/cmd/commands/app.go b/cmd/commands/app.go index bda08811..e1bcccdc 100644 --- a/cmd/commands/app.go +++ b/cmd/commands/app.go @@ -181,6 +181,7 @@ func RunAppCreate(ctx context.Context, opts *AppCreateOptions) error { if opts.AppsCloneOpts.Auth.Password == "" { opts.AppsCloneOpts.Auth.Username = opts.CloneOpts.Auth.Username opts.AppsCloneOpts.Auth.Password = opts.CloneOpts.Auth.Password + opts.AppsCloneOpts.Auth.CertFile = opts.CloneOpts.Auth.CertFile opts.AppsCloneOpts.Provider = opts.CloneOpts.Provider } diff --git a/cmd/commands/repo.go b/cmd/commands/repo.go index 0ce7f10c..ebb6815f 100644 --- a/cmd/commands/repo.go +++ b/cmd/commands/repo.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/url" "os" "path/filepath" "strings" @@ -235,7 +236,7 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { log.G(ctx).Infof("using revision: \"%s\", installation path: \"%s\"", opts.CloneOptions.Revision(), opts.CloneOptions.Path()) err = validateRepo(repofs, opts.Recover) - if err != nil{ + if err != nil { return err } @@ -624,7 +625,7 @@ func buildBootstrapManifests(namespace, appSpecifier string, cloneOpts *git.Clon return nil, err } - k, err := createBootstrapKustomization(namespace, cloneOpts.URL(), appSpecifier) + k, err := createBootstrapKustomization(namespace, appSpecifier, cloneOpts) if err != nil { return nil, err } @@ -691,8 +692,8 @@ func writeManifestsToRepo(repoFS fs.FS, manifests *bootstrapManifests, installat return fsutils.BulkWrite(repoFS, bulkWrites...) } -func createBootstrapKustomization(namespace, repoURL, appSpecifier string) (*kusttypes.Kustomization, error) { - credsYAML, err := createCreds(repoURL) +func createBootstrapKustomization(namespace, appSpecifier string, cloneOpts *git.CloneOptions) (*kusttypes.Kustomization, error) { + credsYAML, err := createCreds(cloneOpts.URL()) if err != nil { return nil, err } @@ -721,6 +722,30 @@ func createBootstrapKustomization(namespace, repoURL, appSpecifier string) (*kus Namespace: namespace, } + cert, err := cloneOpts.Auth.GetCertificate() + if err != nil { + return nil, err + } + + if cert != nil { + u, err := url.Parse(cloneOpts.URL()) + if err != nil { + return nil, err + } + + k.ConfigMapGenerator = append(k.ConfigMapGenerator, kusttypes.ConfigMapArgs{ + GeneratorArgs: kusttypes.GeneratorArgs{ + Name: "argocd-tls-certs-cm", + Behavior: kusttypes.BehaviorMerge.String(), + KvPairSources: kusttypes.KvPairSources{ + LiteralSources: []string{ + u.Host + "=" + string(cert), + }, + }, + }, + }) + } + k.FixKustomizationPostUnmarshalling() errs := k.EnforceFields() if len(errs) > 0 { diff --git a/pkg/git/provider.go b/pkg/git/provider.go index d9474493..d75d2cd7 100644 --- a/pkg/git/provider.go +++ b/pkg/git/provider.go @@ -2,7 +2,11 @@ package git import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" + "os" "sort" ) @@ -26,6 +30,7 @@ type ( Auth struct { Username string Password string + CertFile string } // ProviderOptions for a new git provider @@ -85,3 +90,49 @@ func Providers() []string { sort.Strings(res) // must sort the providers by name, otherwise the codegen is not deterministic return res } + +func DefaultTransportWithCa(certFile string) (*http.Transport, error) { + rootCAs, err := getRootCas(certFile) + if err != nil { + return nil, err + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} + return transport, nil +} + +func getRootCas(certFile string) (*x509.CertPool, error) { + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("failed getting system certificates: %w", err) + } + + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + if certFile == "" { + return rootCAs, nil + } + + certs, err := os.ReadFile(certFile) + if err != nil { + return nil, fmt.Errorf("failed reading certificate from %s: %w", certFile, err) + } + + // Append our cert to the system pool + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + return nil, fmt.Errorf("failed adding certificate to rootCAs") + } + + return rootCAs, nil +} + +func (a *Auth) GetCertificate() ([]byte, error) { + if a.CertFile == "" { + return nil, nil + } + + return os.ReadFile(a.CertFile) +} diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go index 41f7f570..75af0bd4 100644 --- a/pkg/git/provider_ado.go +++ b/pkg/git/provider_ado.go @@ -47,6 +47,12 @@ func newAdo(opts *ProviderOptions) (Provider, error) { } connection := azuredevops.NewPatConnection(adoUrl.loginUrl, opts.Auth.Password) + rootCAs, err := getRootCas(opts.Auth.CertFile) + if err != nil { + return nil, err + } + + connection.TlsConfig.RootCAs = rootCAs ctx, cancel := context.WithTimeout(context.Background(), timeoutTime) defer cancel() // FYI: ado also has a "core" client that can be used to update project, teams, and other ADO constructs diff --git a/pkg/git/provider_bitbucket-server.go b/pkg/git/provider_bitbucket-server.go index cc9d61b6..ad974667 100644 --- a/pkg/git/provider_bitbucket-server.go +++ b/pkg/git/provider_bitbucket-server.go @@ -85,6 +85,11 @@ func newBitbucketServer(opts *ProviderOptions) (Provider, error) { } httpClient := &http.Client{} + httpClient.Transport, err = DefaultTransportWithCa(opts.Auth.CertFile) + if err != nil { + return nil, err + } + g := &bitbucketServer{ baseURL: baseURL, c: httpClient, diff --git a/pkg/git/provider_bitbucket.go b/pkg/git/provider_bitbucket.go index 135d598d..c759fc95 100644 --- a/pkg/git/provider_bitbucket.go +++ b/pkg/git/provider_bitbucket.go @@ -29,10 +29,17 @@ type ( ) func newBitbucket(opts *ProviderOptions) (Provider, error) { + var err error c := bb.NewBasicAuth(opts.Auth.Username, opts.Auth.Password) if c == nil { return nil, errors.New("Authentication info is invalid") } + + c.HttpClient.Transport, err = DefaultTransportWithCa(opts.Auth.CertFile) + if err != nil { + return nil, err + } + g := &bitbucket{ opts: opts, Repository: c.Repositories.Repository, @@ -40,7 +47,6 @@ func newBitbucket(opts *ProviderOptions) (Provider, error) { } return g, nil - } func (g *bitbucket) CreateRepository(ctx context.Context, orgRepo string) (defaultBranch string, err error) { diff --git a/pkg/git/provider_gitea.go b/pkg/git/provider_gitea.go index 32d19c4b..87cd7644 100644 --- a/pkg/git/provider_gitea.go +++ b/pkg/git/provider_gitea.go @@ -3,6 +3,7 @@ package git import ( "context" "fmt" + "net/http" gt "code.gitea.io/sdk/gitea" ) @@ -28,6 +29,15 @@ func newGitea(opts *ProviderOptions) (Provider, error) { return nil, err } + transport, err := DefaultTransportWithCa(opts.Auth.CertFile) + if err != nil { + return nil, err + } + + c.SetHTTPClient(&http.Client{ + Transport: transport, + }) + g := &gitea{ client: c, } diff --git a/pkg/git/provider_github.go b/pkg/git/provider_github.go index ce60f133..089fa7b9 100644 --- a/pkg/git/provider_github.go +++ b/pkg/git/provider_github.go @@ -29,10 +29,18 @@ func newGithub(opts *ProviderOptions) (Provider, error) { hc := &http.Client{} if opts.Auth != nil { - hc.Transport = &gh.BasicAuthTransport{ + underlyingTransport, err := DefaultTransportWithCa(opts.Auth.CertFile) + if err != nil { + return nil, err + } + + transport := &gh.BasicAuthTransport{ Username: opts.Auth.Username, Password: opts.Auth.Password, + Transport: underlyingTransport, } + + hc.Transport = transport } host, _, _, _, _, _, _ := util.ParseGitUrl(opts.RepoURL) diff --git a/pkg/git/provider_gitlab.go b/pkg/git/provider_gitlab.go index 862fbbf9..2fbf61b5 100644 --- a/pkg/git/provider_gitlab.go +++ b/pkg/git/provider_gitlab.go @@ -3,6 +3,7 @@ package git import ( "context" "fmt" + "net/http" "github.com/argoproj-labs/argocd-autopilot/pkg/util" gl "github.com/xanzy/go-gitlab" @@ -32,7 +33,18 @@ type ( func newGitlab(opts *ProviderOptions) (Provider, error) { host, _, _, _, _, _, _ := util.ParseGitUrl(opts.RepoURL) - c, err := gl.NewClient(opts.Auth.Password, gl.WithBaseURL(host)) + transport, err := DefaultTransportWithCa(opts.Auth.CertFile) + if err != nil { + return nil, err + } + + c, err := gl.NewClient( + opts.Auth.Password, + gl.WithBaseURL(host), + gl.WithHTTPClient(&http.Client{ + Transport: transport, + }), + ) if err != nil { return nil, err } diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 3d2ee50d..e4b1e691 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -156,11 +156,13 @@ func AddFlags(cmd *cobra.Command, opts *AddFlagsOptions) *CloneOptions { envPrefix := strings.ReplaceAll(strings.ToUpper(opts.Prefix), "-", "_") cmd.PersistentFlags().StringVar(&co.Auth.Password, opts.Prefix+"git-token", "", fmt.Sprintf("Your git provider api token [%sGIT_TOKEN]", envPrefix)) cmd.PersistentFlags().StringVar(&co.Auth.Username, opts.Prefix+"git-user", "", fmt.Sprintf("Your git provider user name [%sGIT_USER] (not required in GitHub)", envPrefix)) + cmd.PersistentFlags().StringVar(&co.Auth.CertFile, opts.Prefix+"git-server-crt", "", fmt.Sprint("Git Server certificate file", envPrefix)) cmd.PersistentFlags().StringVar(&co.Repo, opts.Prefix+"repo", "", fmt.Sprintf("Repository URL [%sGIT_REPO]", envPrefix)) util.Die(viper.BindEnv(opts.Prefix+"git-token", envPrefix+"GIT_TOKEN")) util.Die(viper.BindEnv(opts.Prefix+"git-user", envPrefix+"GIT_USER")) util.Die(viper.BindEnv(opts.Prefix+"repo", envPrefix+"GIT_REPO")) + util.Die(cmd.PersistentFlags().MarkHidden(opts.Prefix + "git-server-crt")) if opts.Prefix == "" { cmd.Flag("git-token").Shorthand = "t" @@ -273,6 +275,11 @@ func (r *repo) Persist(ctx context.Context, opts *PushOptions) (string, error) { progress = r.progress } + cert, err := r.auth.GetCertificate() + if err != nil { + return "", fmt.Errorf("failed reading git certificate file: %w", err) + } + h, err := r.commit(ctx, opts) if err != nil { return "", err @@ -282,6 +289,7 @@ func (r *repo) Persist(ctx context.Context, opts *PushOptions) (string, error) { err = r.PushContext(ctx, &gg.PushOptions{ Auth: getAuth(r.auth), Progress: progress, + CABundle: cert, }) if err == nil || !errors.Is(err, transport.ErrRepositoryNotFound) { break @@ -403,11 +411,17 @@ var clone = func(ctx context.Context, opts *CloneOptions) (*repo, error) { progress = os.Stderr } + cert, err := opts.Auth.GetCertificate() + if err != nil { + return nil, fmt.Errorf("failed reading git certificate file: %w", err) + } + cloneOpts := &gg.CloneOptions{ URL: opts.url, Auth: getAuth(opts.Auth), Depth: 1, Progress: progress, + CABundle: cert, } log.G(ctx).WithField("url", opts.url).Debug("cloning git repo")