From 2ccda4ad78653c279f9804fd55967aeefaed0774 Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Wed, 3 Jul 2024 10:38:02 +0100 Subject: [PATCH] Add support for `.spec.proxySecretRef` for OCIRepository API Signed-off-by: Matheus Pimenta --- api/v1beta2/ocirepository_types.go | 5 + api/v1beta2/zz_generated.deepcopy.go | 5 + ...rce.toolkit.fluxcd.io_ocirepositories.yaml | 11 ++ docs/api/v1beta2/source.md | 30 +++ docs/spec/v1beta2/ocirepositories.md | 33 ++++ .../controller/ocirepository_controller.go | 68 ++++++- .../ocirepository_controller_test.go | 185 ++++++++++++++++++ 7 files changed, 332 insertions(+), 5 deletions(-) diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index 1e8338393..9030fab74 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -116,6 +116,11 @@ type OCIRepositorySpec struct { // +optional CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"` + // ProxySecretRef specifies the Secret containing the proxy configuration + // to use while communicating with the container registry. + // +optional + ProxySecretRef *meta.LocalObjectReference `json:"proxySecretRef,omitempty"` + // Interval at which the OCIRepository URL is checked for updates. // This interval is approximate and may be subject to jitter to ensure // efficient use of resources. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index b62bafecb..e264a12c9 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -779,6 +779,11 @@ func (in *OCIRepositorySpec) DeepCopyInto(out *OCIRepositorySpec) { *out = new(meta.LocalObjectReference) **out = **in } + if in.ProxySecretRef != nil { + in, out := &in.ProxySecretRef, &out.ProxySecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } out.Interval = in.Interval if in.Timeout != nil { in, out := &in.Timeout, &out.Timeout diff --git a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml index 4e2dc576e..a6098b72a 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml @@ -131,6 +131,17 @@ spec: - azure - gcp type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the container registry. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object ref: description: |- The OCI reference to pull and monitor for changes, diff --git a/docs/api/v1beta2/source.md b/docs/api/v1beta2/source.md index 451d83611..3e51da58a 100644 --- a/docs/api/v1beta2/source.md +++ b/docs/api/v1beta2/source.md @@ -1219,6 +1219,21 @@ been deprecated.

+proxySecretRef
+ + +github.com/fluxcd/pkg/apis/meta.LocalObjectReference + + + + +(Optional) +

ProxySecretRef specifies the Secret containing the proxy configuration +to use while communicating with the container registry.

+ + + + interval
@@ -3235,6 +3250,21 @@ been deprecated.

+proxySecretRef
+ +
+github.com/fluxcd/pkg/apis/meta.LocalObjectReference + + + + +(Optional) +

ProxySecretRef specifies the Secret containing the proxy configuration +to use while communicating with the container registry.

+ + + + interval
diff --git a/docs/spec/v1beta2/ocirepositories.md b/docs/spec/v1beta2/ocirepositories.md index aafd6c7fb..0647a9488 100644 --- a/docs/spec/v1beta2/ocirepositories.md +++ b/docs/spec/v1beta2/ocirepositories.md @@ -330,6 +330,39 @@ data: deprecated. If you have any Secrets using these keys and specified in an OCIRepository, the controller will log a deprecation warning. +### Proxy secret reference + +`.spec.proxySecretRef.name` is an optional field used to specify the name of a +Secret that contains the proxy settings for the object. These settings are used +for all the remote operations related to the OCIRepository. +The Secret can contain three keys: + +- `address`, to specify the address of the proxy server. This is a required key. +- `username`, to specify the username to use if the proxy server is protected by + basic authentication. This is an optional key. +- `password`, to specify the password to use if the proxy server is protected by + basic authentication. This is an optional key. + +Example: + +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: http-proxy +type: Opaque +stringData: + address: http://proxy.com + username: mandalorian + password: grogu +``` + +Proxying can also be configured in the source-controller Deployment directly by +using the standard environment variables such as `HTTPS_PROXY`, `ALL_PROXY`, etc. + +`.spec.proxySecretRef.name` takes precedence over all environment variables. + ### Insecure `.spec.insecure` is an optional field to allow connecting to an insecure (HTTP) diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index 3de4faaa7..7921ed62a 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -920,16 +921,40 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *ociv1.OCIRe // transport clones the default transport from remote and when a certSecretRef is specified, // the returned transport will include the TLS client and/or CA certificates. +// If the insecure flag is set, the transport will skip the verification of the server's certificate. +// Additionally, if a proxy is specified, transport will use it. func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *ociv1.OCIRepository) (*http.Transport, error) { transport := remote.DefaultTransport.(*http.Transport).Clone() + tlsConfig, err := r.getTLSConfig(ctx, obj) + if err != nil { + return nil, err + } + if tlsConfig != nil { + transport.TLSClientConfig = tlsConfig + } + + proxyURL, err := r.getProxyURL(ctx, obj) + if err != nil { + return nil, err + } + if proxyURL != nil { + transport.Proxy = http.ProxyURL(proxyURL) + } + + return transport, nil +} + +// getTLSConfig gets the TLS configuration for the transport based on the +// specified secret reference in the OCIRepository object, or the insecure flag. +func (r *OCIRepositoryReconciler) getTLSConfig(ctx context.Context, obj *ociv1.OCIRepository) (*cryptotls.Config, error) { if obj.Spec.CertSecretRef == nil || obj.Spec.CertSecretRef.Name == "" { if obj.Spec.Insecure { - transport.TLSClientConfig = &cryptotls.Config{ + return &cryptotls.Config{ InsecureSkipVerify: true, - } + }, nil } - return transport, nil + return nil, nil } certSecretName := types.NamespacedName{ @@ -955,9 +980,42 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *ociv1.OCIR Info("warning: specifying TLS auth data via `certFile`/`keyFile`/`caFile` is deprecated, please use `tls.crt`/`tls.key`/`ca.crt` instead") } } - transport.TLSClientConfig = tlsConfig - return transport, nil + return tlsConfig, nil +} + +// getProxyURL gets the proxy configuration for the transport based on the +// specified proxy secret reference in the OCIRepository object. +func (r *OCIRepositoryReconciler) getProxyURL(ctx context.Context, obj *ociv1.OCIRepository) (*url.URL, error) { + if obj.Spec.ProxySecretRef == nil || obj.Spec.ProxySecretRef.Name == "" { + return nil, nil + } + + proxySecretName := types.NamespacedName{ + Namespace: obj.Namespace, + Name: obj.Spec.ProxySecretRef.Name, + } + var proxySecret corev1.Secret + if err := r.Get(ctx, proxySecretName, &proxySecret); err != nil { + return nil, err + } + + proxyData := proxySecret.Data + address, ok := proxyData["address"] + if !ok { + return nil, fmt.Errorf("invalid proxy secret '%s/%s': key 'address' is missing", + obj.Spec.ProxySecretRef.Name, obj.Namespace) + } + proxyURL, err := url.Parse(string(address)) + if err != nil { + return nil, fmt.Errorf("failed to parse proxy address '%s': %w", address, err) + } + user, hasUser := proxyData["username"] + password, hasPassword := proxyData["password"] + if hasUser || hasPassword { + proxyURL.User = url.UserPassword(string(user), string(password)) + } + return proxyURL, nil } // reconcileStorage ensures the current state of the storage matches the diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go index 0e9f89885..fedd1f633 100644 --- a/internal/controller/ocirepository_controller_test.go +++ b/internal/controller/ocirepository_controller_test.go @@ -3511,3 +3511,188 @@ func TestOCIContentConfigChanged(t *testing.T) { }) } } + +func TestOCIRepositoryReconciler_getProxyURL(t *testing.T) { + tests := []struct { + name string + ociRepo *ociv1.OCIRepository + objects []client.Object + expectedURL string + expectedErr string + }{ + { + name: "empty proxySecretRef", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: nil, + }, + }, + }, + { + name: "non-existing proxySecretRef", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "non-existing", + }, + }, + }, + expectedErr: "secrets \"non-existing\" not found", + }, + { + name: "missing address in proxySecretRef", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{}, + }, + }, + expectedErr: "invalid proxy secret 'dummy/': key 'address' is missing", + }, + { + name: "invalid address in proxySecretRef", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{ + "address": {0x7f}, + }, + }, + }, + expectedErr: "failed to parse proxy address '\x7f': parse \"\\x7f\": net/url: invalid control character in URL", + }, + { + name: "no user, no password", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{ + "address": []byte("http://proxy.example.com"), + }, + }, + }, + expectedURL: "http://proxy.example.com", + }, + { + name: "user, no password", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{ + "address": []byte("http://proxy.example.com"), + "username": []byte("user"), + }, + }, + }, + expectedURL: "http://user:@proxy.example.com", + }, + { + name: "no user, password", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{ + "address": []byte("http://proxy.example.com"), + "password": []byte("password"), + }, + }, + }, + expectedURL: "http://:password@proxy.example.com", + }, + { + name: "user, password", + ociRepo: &ociv1.OCIRepository{ + Spec: ociv1.OCIRepositorySpec{ + ProxySecretRef: &meta.LocalObjectReference{ + Name: "dummy", + }, + }, + }, + objects: []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dummy", + }, + Data: map[string][]byte{ + "address": []byte("http://proxy.example.com"), + "username": []byte("user"), + "password": []byte("password"), + }, + }, + }, + expectedURL: "http://user:password@proxy.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + c := fakeclient.NewClientBuilder(). + WithScheme(testEnv.Scheme()). + WithObjects(tt.objects...). + Build() + + r := &OCIRepositoryReconciler{ + Client: c, + } + + u, err := r.getProxyURL(ctx, tt.ociRepo) + if tt.expectedErr == "" { + g.Expect(err).To(BeNil()) + } else { + g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) + } + if tt.expectedURL == "" { + g.Expect(u).To(BeNil()) + } else { + g.Expect(u.String()).To(Equal(tt.expectedURL)) + } + }) + } +}