Merge pull request #1536 from matheuscscp/ocirepo-proxy
Add proxy support for OCIRepository API
This commit is contained in:
commit
7c4fdd5f36
|
@ -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.
|
||||
|
|
|
@ -799,6 +799,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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1235,6 +1235,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">
|
||||
|
@ -3313,6 +3328,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">
|
||||
|
|
|
@ -330,6 +330,47 @@ 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.
|
||||
|
||||
**Warning:** [Cosign](https://github.com/sigstore/cosign) *keyless*
|
||||
[verification](#verification) is not supported for this API. If you
|
||||
require cosign keyless verification to use a proxy you must use the
|
||||
standard environment variables mentioned above. If you specify a
|
||||
`proxySecretRef` the controller will simply send out the requests
|
||||
needed for keyless verification without the associated object-level
|
||||
proxy settings.
|
||||
|
||||
### Insecure
|
||||
|
||||
`.spec.insecure` is an optional field to allow connecting to an insecure (HTTP)
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
@ -437,7 +438,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
|
|||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
||||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
|
||||
|
||||
result, err := r.verifySignature(ctx, obj, ref, keychain, auth, opts...)
|
||||
result, err := r.verifySignature(ctx, obj, ref, keychain, auth, transport, opts...)
|
||||
if err != nil {
|
||||
provider := obj.Spec.Verify.Provider
|
||||
if obj.Spec.Verify.SecretRef == nil && obj.Spec.Verify.Provider == "cosign" {
|
||||
|
@ -623,7 +624,10 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
|
|||
// If not, when using cosign it falls back to a keyless approach for verification.
|
||||
// When notation is used, a trust policy is required to verify the image.
|
||||
// The verification result is returned as a VerificationResult and any error encountered.
|
||||
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv1.OCIRepository, ref name.Reference, keychain authn.Keychain, auth authn.Authenticator, opt ...remote.Option) (soci.VerificationResult, error) {
|
||||
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv1.OCIRepository,
|
||||
ref name.Reference, keychain authn.Keychain, auth authn.Authenticator,
|
||||
transport *http.Transport, opt ...remote.Option) (soci.VerificationResult, error) {
|
||||
|
||||
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
|
@ -753,6 +757,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv
|
|||
notation.WithInsecureRegistry(obj.Spec.Insecure),
|
||||
notation.WithLogger(ctrl.LoggerFrom(ctx)),
|
||||
notation.WithRootCertificates(certs),
|
||||
notation.WithTransport(transport),
|
||||
}
|
||||
|
||||
verifier, err := notation.NewNotationVerifier(defaultNotationOciOpts...)
|
||||
|
@ -920,16 +925,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 +984,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
|
||||
|
|
|
@ -73,6 +73,7 @@ import (
|
|||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
snotation "github.com/fluxcd/source-controller/internal/oci/notation"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
testproxy "github.com/fluxcd/source-controller/tests/proxy"
|
||||
)
|
||||
|
||||
func TestOCIRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
|
||||
|
@ -963,7 +964,133 @@ func TestOCIRepository_CertSecret(t *testing.T) {
|
|||
return len(resultobj.Finalizers) > 0
|
||||
}, timeout).Should(BeTrue())
|
||||
|
||||
// Wait for the object to fail
|
||||
// Wait for the object to be ready
|
||||
g.Eventually(func() bool {
|
||||
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
||||
return false
|
||||
}
|
||||
readyCondition := conditions.Get(&resultobj, meta.ReadyCondition)
|
||||
if readyCondition == nil || conditions.IsUnknown(&resultobj, meta.ReadyCondition) {
|
||||
return false
|
||||
}
|
||||
return obj.Generation == readyCondition.ObservedGeneration &&
|
||||
conditions.IsReady(&resultobj) == tt.expectreadyconition
|
||||
}, timeout).Should(BeTrue())
|
||||
|
||||
tt.expectedstatusmessage = strings.ReplaceAll(tt.expectedstatusmessage, "<url>", pi.url)
|
||||
|
||||
readyCondition := conditions.Get(&resultobj, meta.ReadyCondition)
|
||||
g.Expect(readyCondition.Message).Should(ContainSubstring(tt.expectedstatusmessage))
|
||||
|
||||
// Wait for the object to be deleted
|
||||
g.Expect(testEnv.Delete(ctx, &resultobj)).To(Succeed())
|
||||
g.Eventually(func() bool {
|
||||
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
||||
return apierrors.IsNotFound(err)
|
||||
}
|
||||
return false
|
||||
}, timeout).Should(BeTrue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_ProxySecret(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
regServer, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
t.Cleanup(func() {
|
||||
regServer.Close()
|
||||
})
|
||||
|
||||
pi, err := createPodinfoImageFromTar("podinfo-6.1.5.tar", "6.1.5", regServer.registryHost)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
proxyAddr, proxyPort := testproxy.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
digest gcrv1.Hash
|
||||
proxySecret *corev1.Secret
|
||||
expectreadyconition bool
|
||||
expectedstatusmessage string
|
||||
}{
|
||||
{
|
||||
name: "test proxied connection",
|
||||
url: pi.url,
|
||||
digest: pi.digest,
|
||||
proxySecret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"address": []byte(fmt.Sprintf("http://%s", proxyAddr)),
|
||||
},
|
||||
},
|
||||
expectreadyconition: true,
|
||||
expectedstatusmessage: fmt.Sprintf("stored artifact for digest '%s'", pi.digest.String()),
|
||||
},
|
||||
{
|
||||
name: "test proxy connection error",
|
||||
url: pi.url,
|
||||
digest: pi.digest,
|
||||
proxySecret: &corev1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"address": []byte(fmt.Sprintf("http://localhost:%d", proxyPort+1)),
|
||||
},
|
||||
},
|
||||
expectreadyconition: false,
|
||||
expectedstatusmessage: "failed to pull artifact",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ns, err := testEnv.CreateNamespace(ctx, "ocirepository-test")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }()
|
||||
|
||||
obj := &ociv1.OCIRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "ocirepository-test-resource",
|
||||
Namespace: ns.Name,
|
||||
Generation: 1,
|
||||
},
|
||||
Spec: ociv1.OCIRepositorySpec{
|
||||
URL: tt.url,
|
||||
Interval: metav1.Duration{Duration: 60 * time.Minute},
|
||||
Reference: &ociv1.OCIRepositoryRef{Digest: tt.digest.String()},
|
||||
},
|
||||
}
|
||||
|
||||
if tt.proxySecret != nil {
|
||||
tt.proxySecret.ObjectMeta = metav1.ObjectMeta{
|
||||
GenerateName: "proxy-secretref",
|
||||
Namespace: ns.Name,
|
||||
}
|
||||
|
||||
g.Expect(testEnv.CreateAndWait(ctx, tt.proxySecret)).To(Succeed())
|
||||
defer func() { g.Expect(testEnv.Delete(ctx, tt.proxySecret)).To(Succeed()) }()
|
||||
|
||||
obj.Spec.ProxySecretRef = &meta.LocalObjectReference{Name: tt.proxySecret.Name}
|
||||
}
|
||||
|
||||
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
||||
|
||||
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
||||
|
||||
resultobj := ociv1.OCIRepository{}
|
||||
|
||||
// Wait for the finalizer to be set
|
||||
g.Eventually(func() bool {
|
||||
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
||||
return false
|
||||
}
|
||||
return len(resultobj.Finalizers) > 0
|
||||
}, timeout).Should(BeTrue())
|
||||
|
||||
// Wait for the object to be ready
|
||||
g.Eventually(func() bool {
|
||||
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
||||
return false
|
||||
|
@ -3511,3 +3638,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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,21 @@ limitations under the License.
|
|||
package cosign
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
||||
|
||||
testproxy "github.com/fluxcd/source-controller/tests/proxy"
|
||||
testregistry "github.com/fluxcd/source-controller/tests/registry"
|
||||
)
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
|
@ -128,3 +136,58 @@ func TestOptions(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyVerificationWithProxy(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
registryAddr := testregistry.New(t)
|
||||
|
||||
tagURL := fmt.Sprintf("%s/fluxcd/source-controller:v1.3.0", registryAddr)
|
||||
ref, err := name.ParseReference(tagURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
proxyAddr, proxyPort := testproxy.New(t)
|
||||
|
||||
keys, err := cosign.GenerateKeyPair(func(b bool) ([]byte, error) {
|
||||
return []byte("cosign-password"), nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
proxyURL *url.URL
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "with correct proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: proxyAddr},
|
||||
err: "image tag not found",
|
||||
},
|
||||
{
|
||||
name: "with incorrect proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)},
|
||||
err: "connection refused",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = http.ProxyURL(tt.proxyURL)
|
||||
|
||||
var opts []Options
|
||||
opts = append(opts, WithRemoteOptions(remote.WithTransport(transport)))
|
||||
opts = append(opts, WithPublicKey(keys.PublicBytes))
|
||||
|
||||
verifier, err := NewCosignVerifier(ctx, opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = verifier.Verify(ctx, ref)
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ type options struct {
|
|||
keychain authn.Keychain
|
||||
insecure bool
|
||||
logger logr.Logger
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
// Options is a function that configures the options applied to a Verifier.
|
||||
|
@ -118,14 +119,22 @@ func WithLogger(logger logr.Logger) Options {
|
|||
}
|
||||
}
|
||||
|
||||
// WithTransport is a function that returns an Options function to set the transport for the options.
|
||||
func WithTransport(transport *http.Transport) Options {
|
||||
return func(o *options) {
|
||||
o.transport = transport
|
||||
}
|
||||
}
|
||||
|
||||
// NotationVerifier is a struct which is responsible for executing verification logic
|
||||
type NotationVerifier struct {
|
||||
auth authn.Authenticator
|
||||
keychain authn.Keychain
|
||||
verifier *notation.Verifier
|
||||
opts []remote.Option
|
||||
insecure bool
|
||||
logger logr.Logger
|
||||
auth authn.Authenticator
|
||||
keychain authn.Keychain
|
||||
verifier *notation.Verifier
|
||||
opts []remote.Option
|
||||
insecure bool
|
||||
logger logr.Logger
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
var _ truststore.X509TrustStore = &trustStore{}
|
||||
|
@ -181,12 +190,13 @@ func NewNotationVerifier(opts ...Options) (*NotationVerifier, error) {
|
|||
}
|
||||
|
||||
return &NotationVerifier{
|
||||
auth: o.auth,
|
||||
keychain: o.keychain,
|
||||
verifier: &verifier,
|
||||
opts: o.rOpt,
|
||||
insecure: o.insecure,
|
||||
logger: o.logger,
|
||||
auth: o.auth,
|
||||
keychain: o.keychain,
|
||||
verifier: &verifier,
|
||||
opts: o.rOpt,
|
||||
insecure: o.insecure,
|
||||
logger: o.logger,
|
||||
transport: o.transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -344,8 +354,14 @@ func (v *NotationVerifier) remoteRepo(repoUrl string) (*oras.Repository, error)
|
|||
}
|
||||
}
|
||||
|
||||
hc := retryhttp.DefaultClient
|
||||
if v.transport != nil {
|
||||
hc = &http.Client{
|
||||
Transport: retryhttp.NewTransport(v.transport),
|
||||
}
|
||||
}
|
||||
repoClient := &oauth.Client{
|
||||
Client: retryhttp.DefaultClient,
|
||||
Client: hc,
|
||||
Header: http.Header{
|
||||
"User-Agent": {"flux"},
|
||||
},
|
||||
|
|
|
@ -17,8 +17,11 @@ limitations under the License.
|
|||
package notation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -31,6 +34,8 @@ import (
|
|||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/internal/oci"
|
||||
testproxy "github.com/fluxcd/source-controller/tests/proxy"
|
||||
testregistry "github.com/fluxcd/source-controller/tests/registry"
|
||||
)
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
|
@ -537,6 +542,61 @@ func TestRepoUrlWithDigest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVerificationWithProxy(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
registryAddr := testregistry.New(t)
|
||||
|
||||
tarFilePath := path.Join("..", "..", "controller", "testdata", "podinfo", "podinfo-6.1.5.tar")
|
||||
_, err := testregistry.CreatePodinfoImageFromTar(tarFilePath, "6.1.5", registryAddr)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
tagURL := fmt.Sprintf("%s/podinfo:6.1.5", registryAddr)
|
||||
ref, err := name.ParseReference(tagURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
proxyAddr, proxyPort := testproxy.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
proxyURL *url.URL
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "with correct proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: proxyAddr},
|
||||
err: "no signature is associated with",
|
||||
},
|
||||
{
|
||||
name: "with incorrect proxy",
|
||||
proxyURL: &url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)},
|
||||
err: "connection refused",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = http.ProxyURL(tt.proxyURL)
|
||||
|
||||
var opts []Options
|
||||
opts = append(opts, WithTransport(transport))
|
||||
opts = append(opts, WithTrustPolicy(dummyPolicyDocument()))
|
||||
opts = append(opts, WithInsecureRegistry(true))
|
||||
|
||||
verifier, err := NewNotationVerifier(opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
_, err = verifier.Verify(ctx, ref)
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc *trustpolicy.Document) {
|
||||
policyDoc = &trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
|
@ -548,7 +608,7 @@ func dummyPolicyDocument() (policyDoc *trustpolicy.Document) {
|
|||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||
policyStatement = trustpolicy.TrustPolicy{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
func New(t *testing.T) (net.Listener, string, int) {
|
||||
t.Helper()
|
||||
|
||||
lis, err := net.Listen("tcp", ":0")
|
||||
lis, err := net.Listen("tcp", "localhost:0")
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() { lis.Close() })
|
||||
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright 2024 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
"github.com/distribution/distribution/v3/registry"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gotest.tools/assert"
|
||||
|
||||
"github.com/fluxcd/pkg/oci"
|
||||
|
||||
testlistener "github.com/fluxcd/source-controller/tests/listener"
|
||||
)
|
||||
|
||||
func New(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
// Get a free random port and release it so the registry can use it.
|
||||
listener, addr, _ := testlistener.New(t)
|
||||
err := listener.Close()
|
||||
assert.NilError(t, err)
|
||||
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = addr
|
||||
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
|
||||
config.Log.AccessLog.Disabled = true
|
||||
config.Log.Level = "error"
|
||||
logrus.SetOutput(io.Discard)
|
||||
|
||||
r, err := registry.NewRegistry(context.Background(), config)
|
||||
assert.NilError(t, err)
|
||||
|
||||
go r.ListenAndServe()
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
type PodinfoImage struct {
|
||||
URL string
|
||||
Tag string
|
||||
Digest gcrv1.Hash
|
||||
}
|
||||
|
||||
func CreatePodinfoImageFromTar(tarFilePath, tag, registryURL string, opts ...crane.Option) (*PodinfoImage, error) {
|
||||
// Create Image
|
||||
image, err := crane.Load(tarFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image = setPodinfoImageAnnotations(image, tag)
|
||||
|
||||
// url.Parse doesn't handle urls with no scheme well e.g localhost:<port>
|
||||
if !(strings.HasPrefix(registryURL, "http://") || strings.HasPrefix(registryURL, "https://")) {
|
||||
registryURL = fmt.Sprintf("http://%s", registryURL)
|
||||
}
|
||||
|
||||
myURL, err := url.Parse(registryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repositoryURL := fmt.Sprintf("%s/podinfo", myURL.Host)
|
||||
|
||||
// Image digest
|
||||
podinfoImageDigest, err := image.Digest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Push image
|
||||
err = crane.Push(image, repositoryURL, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Tag the image
|
||||
err = crane.Tag(repositoryURL, tag, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PodinfoImage{
|
||||
URL: "oci://" + repositoryURL,
|
||||
Tag: tag,
|
||||
Digest: podinfoImageDigest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setPodinfoImageAnnotations(img gcrv1.Image, tag string) gcrv1.Image {
|
||||
metadata := map[string]string{
|
||||
oci.SourceAnnotation: "https://github.com/stefanprodan/podinfo",
|
||||
oci.RevisionAnnotation: fmt.Sprintf("%s@sha1:b3b00fe35424a45d373bf4c7214178bc36fd7872", tag),
|
||||
}
|
||||
return mutate.Annotations(img, metadata).(gcrv1.Image)
|
||||
}
|
Loading…
Reference in New Issue