926 lines
29 KiB
Go
926 lines
29 KiB
Go
/*
|
|
Copyright 2022 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 controllers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/darkowlzz/controller-check/status"
|
|
"github.com/fluxcd/pkg/apis/meta"
|
|
"github.com/fluxcd/pkg/runtime/conditions"
|
|
"github.com/fluxcd/pkg/runtime/patch"
|
|
"github.com/fluxcd/pkg/untar"
|
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
"github.com/google/go-containerregistry/pkg/crane"
|
|
"github.com/google/go-containerregistry/pkg/registry"
|
|
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
|
. "github.com/onsi/gomega"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
func TestOCIRepository_Reconcile(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Registry server with public images
|
|
regServer := httptest.NewServer(registry.New())
|
|
versions := []string{"6.1.4", "6.1.5", "6.1.6"}
|
|
podinfoVersions := make(map[string]podinfoImage)
|
|
|
|
for i := 0; i < len(versions); i++ {
|
|
pi, err := createPodinfoImageFromTar(fmt.Sprintf("podinfo-%s.tar", versions[i]), versions[i], regServer)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
podinfoVersions[versions[i]] = *pi
|
|
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
tag string
|
|
semver string
|
|
digest string
|
|
assertArtifact []artifactFixture
|
|
}{
|
|
{
|
|
name: "public tag",
|
|
url: podinfoVersions["6.1.6"].url,
|
|
tag: podinfoVersions["6.1.6"].tag,
|
|
digest: podinfoVersions["6.1.6"].digest.Hex,
|
|
assertArtifact: []artifactFixture{
|
|
{
|
|
expectedPath: "kustomize/deployment.yaml",
|
|
expectedChecksum: "6fd625effe6bb805b6a78943ee082a4412e763edb7fcaed6e8fe644d06cbf423",
|
|
},
|
|
{
|
|
expectedPath: "kustomize/hpa.yaml",
|
|
expectedChecksum: "d20e92e3b2926ebfee1644be0f4d0abadebfa95a8005c12f71bfd534a4be4ff9",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "public semver",
|
|
url: podinfoVersions["6.1.5"].url,
|
|
semver: ">= 6.1 <= 6.1.5",
|
|
digest: podinfoVersions["6.1.5"].digest.Hex,
|
|
assertArtifact: []artifactFixture{
|
|
{
|
|
expectedPath: "kustomize/deployment.yaml",
|
|
expectedChecksum: "dce4f5f780a8e8994b06031e5b567bf488ceaaaabd9bd3fc278b4f3bfc8c577b",
|
|
},
|
|
{
|
|
expectedPath: "kustomize/hpa.yaml",
|
|
expectedChecksum: "d20e92e3b2926ebfee1644be0f4d0abadebfa95a8005c12f71bfd534a4be4ff9",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
ns, err := testEnv.CreateNamespace(ctx, "ocirepository-reconcile-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }()
|
|
|
|
obj := &sourcev1.OCIRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "ocirepository-reconcile",
|
|
Namespace: ns.Name,
|
|
},
|
|
Spec: sourcev1.OCIRepositorySpec{
|
|
URL: tt.url,
|
|
Interval: metav1.Duration{Duration: 60 * time.Minute},
|
|
Reference: &sourcev1.OCIRepositoryRef{},
|
|
},
|
|
}
|
|
|
|
if tt.tag != "" {
|
|
obj.Spec.Reference.Tag = tt.tag
|
|
}
|
|
if tt.semver != "" {
|
|
obj.Spec.Reference.SemVer = tt.semver
|
|
}
|
|
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
|
|
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
|
|
|
// Wait for the finalizer to be set
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
return len(obj.Finalizers) > 0
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for the object to be Ready
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
if !conditions.IsReady(obj) {
|
|
return false
|
|
}
|
|
readyCondition := conditions.Get(obj, meta.ReadyCondition)
|
|
return obj.Generation == readyCondition.ObservedGeneration &&
|
|
obj.Generation == obj.Status.ObservedGeneration
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Check if the revision matches the expected digest
|
|
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest))
|
|
|
|
// Check if the metadata matches the expected annotations
|
|
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.source"]).To(ContainSubstring("podinfo"))
|
|
g.Expect(obj.Status.Artifact.Metadata["org.opencontainers.image.revision"]).To(ContainSubstring(tt.tag))
|
|
|
|
// Check if the artifact storage path matches the expected file path
|
|
localPath := testStorage.LocalPath(*obj.Status.Artifact)
|
|
t.Logf("artifact local path: %s", localPath)
|
|
|
|
f, err := os.Open(localPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer f.Close()
|
|
|
|
// create a tmp directory to extract artifact
|
|
tmp, err := os.MkdirTemp("", "ocirepository-test-")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer os.RemoveAll(tmp)
|
|
|
|
ep, err := untar.Untar(f, tmp)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Logf("extracted summary: %s", ep)
|
|
|
|
for _, af := range tt.assertArtifact {
|
|
expectedFile := filepath.Join(tmp, af.expectedPath)
|
|
g.Expect(expectedFile).To(BeAnExistingFile())
|
|
|
|
f2, err := os.Open(expectedFile)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer f2.Close()
|
|
|
|
h := testStorage.Checksum(f2)
|
|
t.Logf("file %q hash: %q", expectedFile, h)
|
|
g.Expect(h).To(Equal(af.expectedChecksum))
|
|
}
|
|
|
|
// Check if the object status is valid
|
|
condns := &status.Conditions{NegativePolarity: ociRepositoryReadyCondition.NegativePolarity}
|
|
checker := status.NewChecker(testEnv.Client, condns)
|
|
checker.CheckErr(ctx, obj)
|
|
|
|
// kstatus client conformance check
|
|
u, err := patch.ToUnstructured(obj)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
res, err := kstatus.Compute(u)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(res.Status).To(Equal(kstatus.CurrentStatus))
|
|
|
|
// Patch the object with reconcile request annotation.
|
|
patchHelper, err := patch.NewHelper(obj, testEnv.Client)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
annotations := map[string]string{
|
|
meta.ReconcileRequestAnnotation: "now",
|
|
}
|
|
obj.SetAnnotations(annotations)
|
|
g.Expect(patchHelper.Patch(ctx, obj)).ToNot(HaveOccurred())
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
return obj.Status.LastHandledReconcileAt == "now"
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for the object to be deleted
|
|
g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return apierrors.IsNotFound(err)
|
|
}
|
|
return false
|
|
}, timeout).Should(BeTrue())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIRepository_SecretRef(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Instantiate Authenticated Registry Server
|
|
regServer, err := setupRegistryServer(ctx)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Create Test Image
|
|
image, err := crane.Load(path.Join("testdata", "podinfo", "podinfo-6.1.6.tar"))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost)
|
|
ociURL := fmt.Sprintf("oci://%s", repositoryURL)
|
|
|
|
// Push Test Image
|
|
image = setPodinfoImageAnnotations(image, "6.1.6")
|
|
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
|
|
Username: testRegistryUsername,
|
|
Password: testRegistryPassword,
|
|
}))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Test Image digest
|
|
podinfoImageDigest, err := image.Digest()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
digest gcrv1.Hash
|
|
includeSecretRef bool
|
|
includeServiceAccount bool
|
|
}{
|
|
{
|
|
name: "private-registry-access-via-secretref",
|
|
url: ociURL,
|
|
digest: podinfoImageDigest,
|
|
includeSecretRef: true,
|
|
includeServiceAccount: false,
|
|
},
|
|
{
|
|
name: "private-registry-access-via-serviceaccount",
|
|
url: ociURL,
|
|
digest: podinfoImageDigest,
|
|
includeSecretRef: false,
|
|
includeServiceAccount: true,
|
|
},
|
|
}
|
|
|
|
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()) }()
|
|
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "auth-secretref",
|
|
Namespace: ns.Name,
|
|
},
|
|
Type: corev1.SecretTypeDockerConfigJson,
|
|
StringData: map[string]string{
|
|
".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, repositoryURL, testRegistryUsername, testRegistryPassword),
|
|
},
|
|
}
|
|
g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, secret)).To(Succeed()) }()
|
|
|
|
serviceAccount := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "sa-ocitest",
|
|
Namespace: ns.Name,
|
|
},
|
|
ImagePullSecrets: []corev1.LocalObjectReference{{Name: secret.Name}},
|
|
}
|
|
g.Expect(testEnv.CreateAndWait(ctx, serviceAccount)).To(Succeed())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, serviceAccount)).To(Succeed()) }()
|
|
|
|
obj := &sourcev1.OCIRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "ocirepository-test-resource",
|
|
Namespace: ns.Name,
|
|
},
|
|
Spec: sourcev1.OCIRepositorySpec{
|
|
URL: tt.url,
|
|
Interval: metav1.Duration{Duration: 60 * time.Minute},
|
|
Reference: &sourcev1.OCIRepositoryRef{Digest: tt.digest.String()},
|
|
},
|
|
}
|
|
|
|
if tt.includeSecretRef {
|
|
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: secret.Name}
|
|
}
|
|
|
|
if tt.includeServiceAccount {
|
|
obj.Spec.ServiceAccountName = serviceAccount.Name
|
|
}
|
|
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
|
|
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
|
|
|
// Wait for the finalizer to be set
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
return len(obj.Finalizers) > 0
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for the object to be Ready
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
if !conditions.IsReady(obj) {
|
|
return false
|
|
}
|
|
readyCondition := conditions.Get(obj, meta.ReadyCondition)
|
|
return obj.Generation == readyCondition.ObservedGeneration &&
|
|
obj.Generation == obj.Status.ObservedGeneration
|
|
}, timeout).Should(BeTrue())
|
|
|
|
t.Log(obj.Status.Artifact.Revision)
|
|
|
|
// Check if the revision matches the expected digest
|
|
g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest.Hex))
|
|
|
|
// Check if the artifact storage path matches the expected file path
|
|
localPath := testStorage.LocalPath(*obj.Status.Artifact)
|
|
t.Logf("artifact local path: %s", localPath)
|
|
|
|
f, err := os.Open(localPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer f.Close()
|
|
|
|
// create a tmp directory to extract artifact
|
|
tmp, err := os.MkdirTemp("", "ocirepository-test-")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer os.RemoveAll(tmp)
|
|
|
|
ep, err := untar.Untar(f, tmp)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
t.Logf("extracted summary: %s", ep)
|
|
|
|
expectedFile := filepath.Join(tmp, `kustomize/deployment.yaml`)
|
|
g.Expect(expectedFile).To(BeAnExistingFile())
|
|
|
|
f2, err := os.Open(expectedFile)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer f2.Close()
|
|
|
|
h := testStorage.Checksum(f2)
|
|
t.Logf("hash: %q", h)
|
|
g.Expect(h).To(Equal("6fd625effe6bb805b6a78943ee082a4412e763edb7fcaed6e8fe644d06cbf423"))
|
|
|
|
// Check if the object status is valid
|
|
condns := &status.Conditions{NegativePolarity: ociRepositoryReadyCondition.NegativePolarity}
|
|
checker := status.NewChecker(testEnv.Client, condns)
|
|
checker.CheckErr(ctx, obj)
|
|
|
|
// kstatus client conformance check
|
|
u, err := patch.ToUnstructured(obj)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
res, err := kstatus.Compute(u)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(res.Status).To(Equal(kstatus.CurrentStatus))
|
|
|
|
// Patch the object with reconcile request annotation.
|
|
patchHelper, err := patch.NewHelper(obj, testEnv.Client)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
annotations := map[string]string{
|
|
meta.ReconcileRequestAnnotation: "now",
|
|
}
|
|
obj.SetAnnotations(annotations)
|
|
g.Expect(patchHelper.Patch(ctx, obj)).ToNot(HaveOccurred())
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return false
|
|
}
|
|
return obj.Status.LastHandledReconcileAt == "now"
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for the object to be deleted
|
|
g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, obj); err != nil {
|
|
return apierrors.IsNotFound(err)
|
|
}
|
|
return false
|
|
}, timeout).Should(BeTrue())
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIRepository_FailedAuth(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Instantiate Authenticated Registry Server
|
|
regServer, err := setupRegistryServer(ctx)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Create Test Image
|
|
image, err := crane.Load(path.Join("testdata", "podinfo", "podinfo-6.1.6.tar"))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost)
|
|
ociURL := fmt.Sprintf("oci://%s", repositoryURL)
|
|
|
|
// Push Test Image
|
|
image = setPodinfoImageAnnotations(image, "6.1.6")
|
|
err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{
|
|
Username: testRegistryUsername,
|
|
Password: testRegistryPassword,
|
|
}))
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Test Image digest
|
|
podinfoImageDigest, err := image.Digest()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
digest gcrv1.Hash
|
|
repoUsername string
|
|
repoPassword string
|
|
includeSecretRef bool
|
|
includeServiceAccount bool
|
|
}{
|
|
{
|
|
name: "missing-auth",
|
|
url: ociURL,
|
|
repoUsername: "",
|
|
repoPassword: "",
|
|
digest: podinfoImageDigest,
|
|
includeSecretRef: false,
|
|
includeServiceAccount: false,
|
|
},
|
|
{
|
|
name: "invalid-auth-via-secret",
|
|
url: ociURL,
|
|
repoUsername: "InvalidUser",
|
|
repoPassword: "InvalidPassword",
|
|
digest: podinfoImageDigest,
|
|
includeSecretRef: true,
|
|
includeServiceAccount: false,
|
|
},
|
|
{
|
|
name: "invalid-auth-via-service-account",
|
|
url: ociURL,
|
|
repoUsername: "InvalidUser",
|
|
repoPassword: "InvalidPassword",
|
|
digest: podinfoImageDigest,
|
|
includeSecretRef: false,
|
|
includeServiceAccount: true,
|
|
},
|
|
}
|
|
|
|
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()) }()
|
|
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "auth-secretref",
|
|
Namespace: ns.Name,
|
|
},
|
|
Type: corev1.SecretTypeDockerConfigJson,
|
|
StringData: map[string]string{
|
|
".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, repositoryURL, tt.repoUsername, tt.repoPassword),
|
|
},
|
|
}
|
|
g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, secret)).To(Succeed()) }()
|
|
|
|
serviceAccount := &corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "sa-ocitest",
|
|
Namespace: ns.Name,
|
|
},
|
|
ImagePullSecrets: []corev1.LocalObjectReference{{Name: secret.Name}},
|
|
}
|
|
g.Expect(testEnv.CreateAndWait(ctx, serviceAccount)).To(Succeed())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, serviceAccount)).To(Succeed()) }()
|
|
|
|
obj := &sourcev1.OCIRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "ocirepository-test-resource",
|
|
Namespace: ns.Name,
|
|
},
|
|
Spec: sourcev1.OCIRepositorySpec{
|
|
URL: tt.url,
|
|
Interval: metav1.Duration{Duration: 60 * time.Minute},
|
|
Reference: &sourcev1.OCIRepositoryRef{Digest: tt.digest.String()},
|
|
},
|
|
}
|
|
|
|
if tt.includeSecretRef {
|
|
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: secret.Name}
|
|
}
|
|
|
|
if tt.includeServiceAccount {
|
|
obj.Spec.ServiceAccountName = serviceAccount.Name
|
|
}
|
|
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
|
|
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
|
|
|
failedObj := sourcev1.OCIRepository{}
|
|
|
|
// Wait for the finalizer to be set
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, &failedObj); err != nil {
|
|
return false
|
|
}
|
|
return len(failedObj.Finalizers) > 0
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for the object to fail
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, &failedObj); err != nil {
|
|
return false
|
|
}
|
|
readyCondition := conditions.Get(&failedObj, meta.ReadyCondition)
|
|
if readyCondition == nil {
|
|
return false
|
|
}
|
|
return obj.Generation == readyCondition.ObservedGeneration &&
|
|
!conditions.IsReady(&failedObj)
|
|
}, timeout).Should(BeTrue())
|
|
|
|
g.Expect(testEnv.Get(ctx, key, &failedObj)).To(Succeed())
|
|
readyCondition := conditions.Get(&failedObj, meta.ReadyCondition)
|
|
g.Expect(readyCondition.Status).To(Equal(metav1.ConditionFalse))
|
|
g.Expect(readyCondition.Message).Should(ContainSubstring("UNAUTHORIZED: authentication required; [map[Action:pull Class: Name:podinfo Type:repository]]"))
|
|
|
|
// Wait for the object to be deleted
|
|
g.Expect(testEnv.Delete(ctx, &failedObj)).To(Succeed())
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, &failedObj); err != nil {
|
|
return apierrors.IsNotFound(err)
|
|
}
|
|
return false
|
|
}, timeout).Should(BeTrue())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCIRepository_CertSecret(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
registryServer, err := registry.TLS("localhost")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer registryServer.Close()
|
|
|
|
pi, err := createPodinfoImageFromTar("podinfo-6.1.6.tar", "6.1.6", registryServer)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
ca_cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: registryServer.Certificate().Raw})
|
|
t.Logf("certdata: %v", string(ca_cert))
|
|
|
|
tlsSecretCACert := corev1.Secret{
|
|
StringData: map[string]string{
|
|
CACert: string(ca_cert),
|
|
},
|
|
}
|
|
|
|
srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err := createTLSServer()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
srv.StartTLS()
|
|
defer srv.Close()
|
|
|
|
transport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{},
|
|
}
|
|
// Use the server cert as a CA cert, so the client trusts the
|
|
// server cert. (Only works because the server uses the same
|
|
// cert in both roles).
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(srv.Certificate())
|
|
transport.TLSClientConfig.RootCAs = pool
|
|
transport.TLSClientConfig.Certificates = []tls.Certificate{clientTLSCert}
|
|
|
|
srv.Client().Transport = transport
|
|
pi2, err := createPodinfoImageFromTar("podinfo-6.1.5.tar", "6.1.5", srv)
|
|
g.Expect(err).NotTo(HaveOccurred())
|
|
|
|
tlsSecretClientCert := corev1.Secret{
|
|
StringData: map[string]string{
|
|
CACert: string(rootCertPEM),
|
|
ClientCert: string(clientCertPEM),
|
|
ClientKey: string(clientKeyPEM),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
tag string
|
|
digest gcrv1.Hash
|
|
certSecret *corev1.Secret
|
|
expectreadyconition bool
|
|
expectedstatusmessage string
|
|
}{
|
|
{
|
|
name: "test connection without CACert",
|
|
url: pi.url,
|
|
tag: pi.tag,
|
|
digest: pi.digest,
|
|
certSecret: nil,
|
|
expectreadyconition: false,
|
|
expectedstatusmessage: "unexpected status code 400 Bad Request: Client sent an HTTP request to an HTTPS server.",
|
|
},
|
|
{
|
|
name: "test connection with CACert",
|
|
url: pi.url,
|
|
tag: pi.tag,
|
|
digest: pi.digest,
|
|
certSecret: &tlsSecretCACert,
|
|
expectreadyconition: true,
|
|
expectedstatusmessage: fmt.Sprintf("stored artifact for digest '%s'", pi.digest.Hex),
|
|
},
|
|
{
|
|
name: "test connection with CACert, Client Cert and Private Key",
|
|
url: pi2.url,
|
|
tag: pi2.tag,
|
|
digest: pi2.digest,
|
|
certSecret: &tlsSecretClientCert,
|
|
expectreadyconition: true,
|
|
expectedstatusmessage: fmt.Sprintf("stored artifact for digest '%s'", pi2.digest.Hex),
|
|
},
|
|
}
|
|
|
|
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 := &sourcev1.OCIRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "ocirepository-test-resource",
|
|
Namespace: ns.Name,
|
|
},
|
|
Spec: sourcev1.OCIRepositorySpec{
|
|
URL: tt.url,
|
|
Interval: metav1.Duration{Duration: 60 * time.Minute},
|
|
Reference: &sourcev1.OCIRepositoryRef{Digest: tt.digest.String()},
|
|
},
|
|
}
|
|
|
|
if tt.certSecret != nil {
|
|
tt.certSecret.ObjectMeta = metav1.ObjectMeta{
|
|
GenerateName: "cert-secretref",
|
|
Namespace: ns.Name,
|
|
}
|
|
|
|
g.Expect(testEnv.CreateAndWait(ctx, tt.certSecret)).To(Succeed())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, tt.certSecret)).To(Succeed()) }()
|
|
|
|
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: tt.certSecret.Name}
|
|
}
|
|
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
|
|
key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
|
|
|
|
resultobj := sourcev1.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 fail
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, key, &resultobj); err != nil {
|
|
return false
|
|
}
|
|
readyCondition := conditions.Get(&resultobj, meta.ReadyCondition)
|
|
if readyCondition == nil {
|
|
return false
|
|
}
|
|
return obj.Generation == readyCondition.ObservedGeneration &&
|
|
conditions.IsReady(&resultobj) == tt.expectreadyconition
|
|
}, timeout).Should(BeTrue())
|
|
|
|
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())
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
type artifactFixture struct {
|
|
expectedPath string
|
|
expectedChecksum string
|
|
}
|
|
type podinfoImage struct {
|
|
url string
|
|
tag string
|
|
digest gcrv1.Hash
|
|
}
|
|
|
|
func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Server) (*podinfoImage, error) {
|
|
// Create Image
|
|
image, err := crane.Load(path.Join("testdata", "podinfo", tarFileName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
image = setPodinfoImageAnnotations(image, tag)
|
|
|
|
url, err := url.Parse(imageServer.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repositoryURL := fmt.Sprintf("%s/podinfo", url.Host)
|
|
|
|
// Image digest
|
|
podinfoImageDigest, err := image.Digest()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Push image
|
|
err = crane.Push(image, repositoryURL, crane.WithTransport(imageServer.Client().Transport))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Tag the image
|
|
err = crane.Tag(repositoryURL, tag, crane.WithTransport(imageServer.Client().Transport))
|
|
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{
|
|
"org.opencontainers.image.source": "https://github.com/stefanprodan/podinfo",
|
|
"org.opencontainers.image.revision": fmt.Sprintf("%s/SHA", tag),
|
|
}
|
|
return mutate.Annotations(img, metadata).(gcrv1.Image)
|
|
}
|
|
|
|
// These two taken verbatim from https://ericchiang.github.io/post/go-tls/
|
|
func certTemplate() (*x509.Certificate, error) {
|
|
// generate a random serial number (a real cert authority would
|
|
// have some logic behind this)
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
if err != nil {
|
|
return nil, errors.New("failed to generate serial number: " + err.Error())
|
|
}
|
|
|
|
tmpl := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{Organization: []string{"Flux project"}},
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Hour), // valid for an hour
|
|
BasicConstraintsValid: true,
|
|
}
|
|
return &tmpl, nil
|
|
}
|
|
|
|
func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (
|
|
cert *x509.Certificate, certPEM []byte, err error) {
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// parse the resulting certificate so we can use it again
|
|
cert, err = x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// PEM encode the certificate (this is a standard TLS encoding)
|
|
b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
|
|
certPEM = pem.EncodeToMemory(&b)
|
|
return
|
|
}
|
|
|
|
func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificate, error) {
|
|
var clientTLSCert tls.Certificate
|
|
var rootCertPEM, clientCertPEM, clientKeyPEM []byte
|
|
|
|
srv := httptest.NewUnstartedServer(registry.New())
|
|
|
|
// Create a self-signed cert to use as the CA and server cert.
|
|
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
rootCertTmpl, err := certTemplate()
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
rootCertTmpl.IsCA = true
|
|
rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
|
|
rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
|
rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
|
|
var rootCert *x509.Certificate
|
|
rootCert, rootCertPEM, err = createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
|
|
rootKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey),
|
|
})
|
|
|
|
// Create a TLS cert using the private key and certificate.
|
|
rootTLSCert, err := tls.X509KeyPair(rootCertPEM, rootKeyPEM)
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
|
|
// To trust a client certificate, the server must be given a
|
|
// CA cert pool.
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(rootCert)
|
|
|
|
srv.TLS = &tls.Config{
|
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
|
Certificates: []tls.Certificate{rootTLSCert},
|
|
ClientCAs: pool,
|
|
}
|
|
|
|
// Create a client cert, signed by the "CA".
|
|
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
clientCertTmpl, err := certTemplate()
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
|
|
clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
|
_, clientCertPEM, err = createCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey)
|
|
if err != nil {
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|
|
// Encode and load the cert and private key for the client.
|
|
clientKeyPEM = pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
|
|
})
|
|
clientTLSCert, err = tls.X509KeyPair(clientCertPEM, clientKeyPEM)
|
|
return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err
|
|
}
|