diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 072e7232b..c2738f1bd 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -121,16 +121,39 @@ make deploy Create a `.vscode/launch.json` file: ```json { - "version": "0.2.0", +"version": "0.2.0", "configurations": [ { - "name": "Launch Package", + "name": "Debug Test Function", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/internal/controller/", + "env": { + "HTTPS_PROXY": "", + "HTTP_PROXY": "", + "KUBEBUILDER_ASSETS": "${workspaceFolder}/build/testbin/k8s/1.28.0-linux-amd64/", + "GIT_CONFIG_GLOBAL":"/dev/null", + "GIT_CONFIG_NOSYSTEM":"true", + + }, + "args": [ + "-test.run", "^.*", + "-test.v" + ] + }, + { + "name": "Debug", "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/main.go" + "program": "${workspaceFolder}/main.go", + "args": [ + "--storage-adv-addr=:0", + "--storage-path=/tmp/" + ] } - ] + ], } ``` diff --git a/internal/controller/auth_certificate.go b/internal/controller/auth_certificate.go new file mode 100644 index 000000000..795466fc7 --- /dev/null +++ b/internal/controller/auth_certificate.go @@ -0,0 +1,45 @@ +package controller + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + + "github.com/go-git/go-git/v5/plumbing/transport" + httptransport "github.com/go-git/go-git/v5/plumbing/transport/http" + ctrl "sigs.k8s.io/controller-runtime" +) + +// HttpTransportWithCustomCerts returns an HTTP transport with custom certificates. +// If proxyStr is provided, it will be used as the proxy URL. +// If not, it tries to fetch the proxy from an environment variable. +func HttpTransportwithCustomCerts(tlsConfig *tls.Config, proxyStr *transport.ProxyOptions, ctx context.Context) (transport.Transport, error) { + + log := ctrl.LoggerFrom(ctx) + // var message string + + var ( + proxyUrl *url.URL + err error + ) + if proxyStr != nil { + proxyUrl, err = url.Parse(proxyStr.URL) + if err != nil { + log.Info("failed to parse proxy url: %w", err) + } + } + + if tlsConfig == nil || len(tlsConfig.Certificates) == 0 { + log.Info("tlsConfig cannot be nil") + return nil, nil + } + + return httptransport.NewClient(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), + TLSClientConfig: tlsConfig, + }, + }), nil + +} diff --git a/internal/controller/gitrepository_controller.go b/internal/controller/gitrepository_controller.go index 2440904a2..bc11529fd 100644 --- a/internal/controller/gitrepository_controller.go +++ b/internal/controller/gitrepository_controller.go @@ -61,7 +61,9 @@ import ( "github.com/fluxcd/source-controller/internal/features" sreconcile "github.com/fluxcd/source-controller/internal/reconcile" "github.com/fluxcd/source-controller/internal/reconcile/summarize" + "github.com/fluxcd/source-controller/internal/tls" "github.com/fluxcd/source-controller/internal/util" + gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" ) // gitRepositoryReadyCondition contains the information required to summarize a @@ -148,6 +150,94 @@ func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { return r.SetupWithManagerAndOptions(mgr, GitRepositoryReconcilerOptions{}) } +// Interface co configure gitclient with custom TLS options +// used for application firewall authentication. +type GitClientConfigurer interface { + ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) + IsValid() bool +} + +type GitClientHttpConfigurer struct { + SSLCertificateData map[string][]byte + ProxyOpts *transport.ProxyOptions + Valid bool + DefaultTransport transport.Transport + AppFirewallTransport transport.Transport +} + +func (c *GitClientHttpConfigurer) IsValid() bool { + return c.Valid +} + +func (c *GitClientHttpConfigurer) SetValid() { + c.Valid = true +} + +func (c *GitClientHttpConfigurer) SetInvalid() { + c.Valid = false +} + +func (r *GitRepositoryReconciler) isCertificateDataValid(sslCertificateData map[string][]byte) bool { + certBytes, keyBytes := sslCertificateData["certFile"], sslCertificateData["keyFile"] + // Validate that both the certificate and key data are present + return len(certBytes) > 0 && len(keyBytes) > 0 +} + + +func (h *GitClientHttpConfigurer) ConfigureGitClient(ctx context.Context, obj *sourcev1.GitRepository) { + + if obj.Spec.SecretRef != nil { + // var secretName = obj.Spec.SecretRef.Name + // if secretName == "waf-authentication" { + sslCertificate := &corev1.Secret{ + Data: h.SSLCertificateData, + } + tlsConfig, _, err := tls.TLSClientConfigFromSecret(*sslCertificate, "") + if err != nil { + fmt.Println("Error generating TLS config:", err) + return + } + + + transportHttp, err := HttpTransportwithCustomCerts(tlsConfig, h.ProxyOpts, ctx) + if err != nil { + fmt.Println("Error setting up transport:", err) + return + } + + gitclient.InstallProtocol("https", transportHttp) + + } + // } +} + +// configureHttpTransport sets up the HTTP transport configuration for the Git client. +func (r *GitRepositoryReconciler) configureHttpTransport(ctx context.Context, obj *sourcev1.GitRepository) (*GitClientHttpConfigurer, error) { + httpTransportConfig := &GitClientHttpConfigurer{} // Initialize with defaults configuration + + // Check if SecretRef is specified for the repository + if obj.Spec.SecretRef != nil { + // Fetch the SSL certificate data from the specified secret + sslCertificateData, err := r.getSecretData(ctx, obj.Spec.SecretRef.Name, obj.Namespace) + if err != nil { + return nil, fmt.Errorf("failed to get secret '%s/%s': %w", obj.Namespace, obj.Spec.SecretRef.Name, err) + } + + // Set up the HTTP transport configuration with the fetched certificate data + httpTransportConfig.SSLCertificateData = sslCertificateData + if r.isCertificateDataValid(sslCertificateData) { + httpTransportConfig.SetValid() + } else { + httpTransportConfig.SetInvalid() + } + } else { + // If no SecretRef is provided, mark the transport config as invalid or set defaults + httpTransportConfig.SetInvalid() + } + + return httpTransportConfig, nil +} + func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts GitRepositoryReconcilerOptions) error { r.patchOptions = getPatchOptions(gitRepositoryReadyCondition.Owned, r.ControllerName) @@ -535,7 +625,12 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch // Persist the ArtifactSet. *includes = *artifacts - c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true) + httpTransportConfig, err := r.configureHttpTransport(ctx, obj) + if err != nil { + return sreconcile.ResultEmpty, err + } + + c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, true, httpTransportConfig) if err != nil { return sreconcile.ResultEmpty, err } @@ -579,7 +674,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch // If we can't skip the reconciliation, checkout again without any // optimization. - c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false) + c, err := r.gitCheckout(ctx, obj, authOpts, proxyOpts, dir, false, httpTransportConfig) if err != nil { return sreconcile.ResultEmpty, err } @@ -832,7 +927,7 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, sp *patc // gitCheckout builds checkout options with the given configurations and // performs a git checkout. func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1.GitRepository, - authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool) (*git.Commit, error) { + authOpts *git.AuthOptions, proxyOpts *transport.ProxyOptions, dir string, optimized bool, clientConf GitClientConfigurer) (*git.Commit, error) { // Configure checkout strategy. cloneOpts := repository.CloneConfig{ RecurseSubmodules: obj.Spec.RecurseSubmodules, @@ -866,6 +961,9 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, obj *sourcev1 clientOpts = append(clientOpts, gogit.WithProxy(*proxyOpts)) } + if clientConf.IsValid() { + clientConf.ConfigureGitClient(ctx, obj) + } gitReader, err := gogit.NewClient(dir, authOpts, clientOpts...) if err != nil { e := serror.NewGeneric( diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 800c65577..676aa38e4 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -571,6 +571,33 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) { *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), }, }, + { + name: "HTTPS with TLS certs authentication with WAF Reconciling=True", + protocol: "https", + server: options{ + publicKey: tlsPublicKey, + privateKey: tlsPrivateKey, + ca: tlsCA, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "waf-authentication", + }, + Data: map[string][]byte{ + "certFile": clientPublicKey, + "keyFile": clientPrivateKey, + "caFile": tlsCA, + }, + }, + beforeFunc: func(obj *sourcev1.GitRepository) { + obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "waf-authentication"} + }, + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), + *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new upstream revision 'master@sha1:'"), + }, + }, } for _, tt := range tests {