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))
+ }
+ })
+ }
+}
|