Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for .spec.proxySecretRef for OCIRepository API
Browse files Browse the repository at this point in the history
Signed-off-by: Matheus Pimenta <[email protected]>
matheuscscp committed Jul 16, 2024
1 parent c7e8330 commit 1020bbb
Showing 7 changed files with 332 additions and 5 deletions.
5 changes: 5 additions & 0 deletions api/v1beta2/ocirepository_types.go
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
Original file line number Diff line number Diff line change
@@ -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,
30 changes: 30 additions & 0 deletions docs/api/v1beta2/source.md
Original file line number Diff line number Diff line change
@@ -1219,6 +1219,21 @@ been deprecated.</p>
</tr>
<tr>
<td>
<code>proxySecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ProxySecretRef specifies the Secret containing the proxy configuration
to use while communicating with the container registry.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
@@ -3235,6 +3250,21 @@ been deprecated.</p>
</tr>
<tr>
<td>
<code>proxySecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ProxySecretRef specifies the Secret containing the proxy configuration
to use while communicating with the container registry.</p>
</td>
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
33 changes: 33 additions & 0 deletions docs/spec/v1beta2/ocirepositories.md
Original file line number Diff line number Diff line change
@@ -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)
68 changes: 63 additions & 5 deletions internal/controller/ocirepository_controller.go
Original file line number Diff line number Diff line change
@@ -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.Namespace, obj.Spec.ProxySecretRef.Name)
}
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
185 changes: 185 additions & 0 deletions internal/controller/ocirepository_controller_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
}

0 comments on commit 1020bbb

Please sign in to comment.