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
|
// +optional
|
||||||
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
|
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.
|
// Interval at which the OCIRepository URL is checked for updates.
|
||||||
// This interval is approximate and may be subject to jitter to ensure
|
// This interval is approximate and may be subject to jitter to ensure
|
||||||
// efficient use of resources.
|
// efficient use of resources.
|
||||||
|
|
|
@ -799,6 +799,11 @@ func (in *OCIRepositorySpec) DeepCopyInto(out *OCIRepositorySpec) {
|
||||||
*out = new(meta.LocalObjectReference)
|
*out = new(meta.LocalObjectReference)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ProxySecretRef != nil {
|
||||||
|
in, out := &in.ProxySecretRef, &out.ProxySecretRef
|
||||||
|
*out = new(meta.LocalObjectReference)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
out.Interval = in.Interval
|
out.Interval = in.Interval
|
||||||
if in.Timeout != nil {
|
if in.Timeout != nil {
|
||||||
in, out := &in.Timeout, &out.Timeout
|
in, out := &in.Timeout, &out.Timeout
|
||||||
|
|
|
@ -131,6 +131,17 @@ spec:
|
||||||
- azure
|
- azure
|
||||||
- gcp
|
- gcp
|
||||||
type: string
|
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:
|
ref:
|
||||||
description: |-
|
description: |-
|
||||||
The OCI reference to pull and monitor for changes,
|
The OCI reference to pull and monitor for changes,
|
||||||
|
|
|
@ -1235,6 +1235,21 @@ been deprecated.</p>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
<code>interval</code><br>
|
||||||
<em>
|
<em>
|
||||||
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
||||||
|
@ -3313,6 +3328,21 @@ been deprecated.</p>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
<code>interval</code><br>
|
||||||
<em>
|
<em>
|
||||||
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
|
<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
|
deprecated. If you have any Secrets using these keys and specified in an
|
||||||
OCIRepository, the controller will log a deprecation warning.
|
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
|
### Insecure
|
||||||
|
|
||||||
`.spec.insecure` is an optional field to allow connecting to an insecure (HTTP)
|
`.spec.insecure` is an optional field to allow connecting to an insecure (HTTP)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -437,7 +438,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch
|
||||||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
||||||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
|
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 {
|
if err != nil {
|
||||||
provider := obj.Spec.Verify.Provider
|
provider := obj.Spec.Verify.Provider
|
||||||
if obj.Spec.Verify.SecretRef == nil && obj.Spec.Verify.Provider == "cosign" {
|
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.
|
// 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.
|
// 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.
|
// 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)
|
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -753,6 +757,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv
|
||||||
notation.WithInsecureRegistry(obj.Spec.Insecure),
|
notation.WithInsecureRegistry(obj.Spec.Insecure),
|
||||||
notation.WithLogger(ctrl.LoggerFrom(ctx)),
|
notation.WithLogger(ctrl.LoggerFrom(ctx)),
|
||||||
notation.WithRootCertificates(certs),
|
notation.WithRootCertificates(certs),
|
||||||
|
notation.WithTransport(transport),
|
||||||
}
|
}
|
||||||
|
|
||||||
verifier, err := notation.NewNotationVerifier(defaultNotationOciOpts...)
|
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,
|
// 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.
|
// 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) {
|
func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *ociv1.OCIRepository) (*http.Transport, error) {
|
||||||
transport := remote.DefaultTransport.(*http.Transport).Clone()
|
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.CertSecretRef == nil || obj.Spec.CertSecretRef.Name == "" {
|
||||||
if obj.Spec.Insecure {
|
if obj.Spec.Insecure {
|
||||||
transport.TLSClientConfig = &cryptotls.Config{
|
return &cryptotls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
return nil, nil
|
||||||
return transport, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
certSecretName := types.NamespacedName{
|
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")
|
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
|
// reconcileStorage ensures the current state of the storage matches the
|
||||||
|
|
|
@ -73,6 +73,7 @@ import (
|
||||||
serror "github.com/fluxcd/source-controller/internal/error"
|
serror "github.com/fluxcd/source-controller/internal/error"
|
||||||
snotation "github.com/fluxcd/source-controller/internal/oci/notation"
|
snotation "github.com/fluxcd/source-controller/internal/oci/notation"
|
||||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||||
|
testproxy "github.com/fluxcd/source-controller/tests/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOCIRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
|
func TestOCIRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
|
||||||
|
@ -963,7 +964,133 @@ func TestOCIRepository_CertSecret(t *testing.T) {
|
||||||
return len(resultobj.Finalizers) > 0
|
return len(resultobj.Finalizers) > 0
|
||||||
}, timeout).Should(BeTrue())
|
}, 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 {
|
g.Eventually(func() bool {
|
||||||
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
||||||
return false
|
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
|
package cosign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
"github.com/sigstore/cosign/v2/pkg/cosign"
|
"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) {
|
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
|
keychain authn.Keychain
|
||||||
insecure bool
|
insecure bool
|
||||||
logger logr.Logger
|
logger logr.Logger
|
||||||
|
transport *http.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options is a function that configures the options applied to a Verifier.
|
// Options is a function that configures the options applied to a Verifier.
|
||||||
|
@ -118,6 +119,13 @@ 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
|
// NotationVerifier is a struct which is responsible for executing verification logic
|
||||||
type NotationVerifier struct {
|
type NotationVerifier struct {
|
||||||
auth authn.Authenticator
|
auth authn.Authenticator
|
||||||
|
@ -126,6 +134,7 @@ type NotationVerifier struct {
|
||||||
opts []remote.Option
|
opts []remote.Option
|
||||||
insecure bool
|
insecure bool
|
||||||
logger logr.Logger
|
logger logr.Logger
|
||||||
|
transport *http.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ truststore.X509TrustStore = &trustStore{}
|
var _ truststore.X509TrustStore = &trustStore{}
|
||||||
|
@ -187,6 +196,7 @@ func NewNotationVerifier(opts ...Options) (*NotationVerifier, error) {
|
||||||
opts: o.rOpt,
|
opts: o.rOpt,
|
||||||
insecure: o.insecure,
|
insecure: o.insecure,
|
||||||
logger: o.logger,
|
logger: o.logger,
|
||||||
|
transport: o.transport,
|
||||||
}, nil
|
}, 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{
|
repoClient := &oauth.Client{
|
||||||
Client: retryhttp.DefaultClient,
|
Client: hc,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"User-Agent": {"flux"},
|
"User-Agent": {"flux"},
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,8 +17,11 @@ limitations under the License.
|
||||||
package notation
|
package notation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -31,6 +34,8 @@ import (
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/internal/oci"
|
"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) {
|
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) {
|
func dummyPolicyDocument() (policyDoc *trustpolicy.Document) {
|
||||||
policyDoc = &trustpolicy.Document{
|
policyDoc = &trustpolicy.Document{
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
|
@ -548,7 +608,7 @@ func dummyPolicyDocument() (policyDoc *trustpolicy.Document) {
|
||||||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||||
policyStatement = trustpolicy.TrustPolicy{
|
policyStatement = trustpolicy.TrustPolicy{
|
||||||
Name: "test-statement-name",
|
Name: "test-statement-name",
|
||||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
RegistryScopes: []string{"*"},
|
||||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
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"},
|
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) {
|
func New(t *testing.T) (net.Listener, string, int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", ":0")
|
lis, err := net.Listen("tcp", "localhost:0")
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
t.Cleanup(func() { lis.Close() })
|
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