diff --git a/api/repositories/dockercfg/json.go b/api/repositories/dockercfg/json.go deleted file mode 100644 index 8a7f17e63..000000000 --- a/api/repositories/dockercfg/json.go +++ /dev/null @@ -1,35 +0,0 @@ -package dockercfg - -import ( - "encoding/base64" - "encoding/json" -) - -type dockerConfigJSON struct { - Auths map[string]dockerConfigEntry `json:"auths" datapolicy:"token"` -} - -type dockerConfigEntry struct { - Auth string `json:"auth,omitempty"` -} - -func GenerateDockerCfgSecretData(username, password, server string) ([]byte, error) { - if server == "" || server == "index.docker.io" { - server = "https://index.docker.io/v1/" - } - - dockerConfigAuth := dockerConfigEntry{ - Auth: encodeDockerConfigFieldAuth(username, password), - } - result := dockerConfigJSON{ - Auths: map[string]dockerConfigEntry{server: dockerConfigAuth}, - } - - return json.Marshal(result) -} - -// encodeDockerConfigFieldAuth returns base64 encoding of the username and password string -func encodeDockerConfigFieldAuth(username, password string) string { - fieldValue := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(fieldValue)) -} diff --git a/api/repositories/dockercfg/json_test.go b/api/repositories/dockercfg/json_test.go deleted file mode 100644 index a4f4fab2e..000000000 --- a/api/repositories/dockercfg/json_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package dockercfg_test - -import ( - "encoding/base64" - - "code.cloudfoundry.org/korifi/api/repositories/dockercfg" - . "code.cloudfoundry.org/korifi/tests/matchers" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("dockercfgjson", func() { - var ( - server string - dockercfgjson string - ) - - BeforeEach(func() { - server = "myrepo" - }) - - JustBeforeEach(func() { - dockercfgjsonBytes, err := dockercfg.GenerateDockerCfgSecretData("bob", "password", server) - Expect(err).NotTo(HaveOccurred()) - - dockercfgjson = string(dockercfgjsonBytes) - }) - - It("generates a valid dockercfgjson", func() { - Expect(dockercfgjson).To( - MatchJSONPath("$.auths.myrepo.auth", base64.StdEncoding.EncodeToString([]byte("bob:password"))), - ) - }) - - When("the server is not set", func() { - BeforeEach(func() { - server = "" - }) - - It("sets the server to https://index.docker.io/v1/", func() { - Expect(dockercfgjson).To( - MatchJSONPath(`$.auths["https://index.docker.io/v1/"].auth`, base64.StdEncoding.EncodeToString([]byte("bob:password"))), - ) - }) - }) - - When("the server is index.docker.io", func() { - BeforeEach(func() { - server = "index.docker.io" - }) - - It("sets the server to https://index.docker.io/v1/", func() { - Expect(dockercfgjson).To( - MatchJSONPath(`$.auths["https://index.docker.io/v1/"].auth`, base64.StdEncoding.EncodeToString([]byte("bob:password"))), - ) - }) - }) -}) diff --git a/api/repositories/package_repository.go b/api/repositories/package_repository.go index 1b900b8c7..7cc38f434 100644 --- a/api/repositories/package_repository.go +++ b/api/repositories/package_repository.go @@ -8,10 +8,10 @@ import ( "code.cloudfoundry.org/korifi/api/authorization" apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories/conditions" - "code.cloudfoundry.org/korifi/api/repositories/dockercfg" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/controllers/workloads" + "code.cloudfoundry.org/korifi/tools/dockercfg" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/google/go-containerregistry/pkg/name" "github.com/google/uuid" @@ -180,28 +180,26 @@ func createImagePullSecret(ctx context.Context, userClient client.Client, cfPack if err != nil { return fmt.Errorf("failed to parse image ref: %w", err) } - dockerCfg, err := dockercfg.GenerateDockerCfgSecretData(*message.Data.Username, *message.Data.Password, ref.Context().RegistryStr()) - if err != nil { - return fmt.Errorf("failed to generate dockercfgjson: %w", err) - } - imgPullSecret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfPackage.Namespace, - Name: cfPackage.Name, + imgPullSecret, err := dockercfg.CreateDockerConfigSecret( + cfPackage.Namespace, + cfPackage.Name, + dockercfg.DockerServerConfig{ + Server: ref.Context().RegistryStr(), + Username: *message.Data.Username, + Password: *message.Data.Password, }, - Data: map[string][]byte{ - corev1.DockerConfigJsonKey: dockerCfg, - }, - Type: corev1.SecretTypeDockerConfigJson, + ) + if err != nil { + return fmt.Errorf("failed to generate image pull secret: %w", err) } - err = controllerutil.SetOwnerReference(cfPackage, &imgPullSecret, scheme.Scheme) + err = controllerutil.SetOwnerReference(cfPackage, imgPullSecret, scheme.Scheme) if err != nil { return fmt.Errorf("failed to set ownership from the package to the image pull secret: %w", err) } - err = userClient.Create(ctx, &imgPullSecret) + err = userClient.Create(ctx, imgPullSecret) if err != nil { return fmt.Errorf("failed create the image pull secret: %w", err) } diff --git a/controllers/controllers/workloads/cf_docker_build_controller.go b/controllers/controllers/workloads/cf_docker_build_controller.go index 6406cd19e..6a182ca5a 100644 --- a/controllers/controllers/workloads/cf_docker_build_controller.go +++ b/controllers/controllers/workloads/cf_docker_build_controller.go @@ -36,8 +36,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) -//counterfeiter:generate -o fake -fake-name ImageConfigGetter . ImageConfigGetter - type ImageConfigGetter interface { Config(context.Context, image.Creds, string) (image.Config, error) } diff --git a/controllers/controllers/workloads/cf_docker_build_controller_test.go b/controllers/controllers/workloads/cf_docker_build_controller_test.go index f08d69138..fedbeb63e 100644 --- a/controllers/controllers/workloads/cf_docker_build_controller_test.go +++ b/controllers/controllers/workloads/cf_docker_build_controller_test.go @@ -4,12 +4,13 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools" - "code.cloudfoundry.org/korifi/tools/image" + "code.cloudfoundry.org/korifi/tools/dockercfg" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + v1 "github.com/google/go-containerregistry/pkg/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gstruct" @@ -17,6 +18,10 @@ import ( var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { var ( + imageSecret *corev1.Secret + imageConfig *v1.ConfigFile + imageRef string + cfSpace *korifiv1alpha1.CFSpace cfApp *korifiv1alpha1.CFApp cfPackageGUID string @@ -24,14 +29,28 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { ) BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{ - Labels: map[string]string{}, - ExposedPorts: []int32{}, - User: "1000", - }, nil) + imageRef = containerRegistry.ImageRef("foo/bar") + imageConfig = &v1.ConfigFile{ + Config: v1.Config{ + User: "1000", + }, + } cfSpace = createSpace(cfOrg) + var err error + imageSecret, err = dockercfg.CreateDockerConfigSecret( + cfSpace.Status.GUID, + PrefixedGUID("image-secret"), + dockercfg.DockerServerConfig{ + Server: containerRegistry.URL(), + Username: "user", + Password: "password", + }, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(adminClient.Create(ctx, imageSecret)).To(Succeed()) + cfApp = &korifiv1alpha1.CFApp{ ObjectMeta: metav1.ObjectMeta{ Name: PrefixedGUID("cf-app"), @@ -61,8 +80,8 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { }, Source: korifiv1alpha1.PackageSource{ Registry: korifiv1alpha1.Registry{ - Image: "some/image", - ImagePullSecrets: []corev1.LocalObjectReference{{Name: "source-image-secret"}}, + Image: imageRef, + ImagePullSecrets: []corev1.LocalObjectReference{{Name: imageSecret.Name}}, }, }, }, @@ -88,6 +107,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { }) JustBeforeEach(func() { + containerRegistry.PushImage(containerRegistry.ImageRef("foo/bar"), imageConfig) Expect(adminClient.Create(ctx, cfBuild)).To(Succeed()) }) @@ -134,21 +154,11 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { g.Expect(meta.IsStatusConditionFalse(cfBuild.Status.Conditions, korifiv1alpha1.StagingConditionType)).To(BeTrue()) g.Expect(meta.IsStatusConditionTrue(cfBuild.Status.Conditions, korifiv1alpha1.SucceededConditionType)).To(BeTrue()) g.Expect(cfBuild.Status.Droplet).NotTo(BeNil()) - g.Expect(cfBuild.Status.Droplet.Registry.Image).To(Equal("some/image")) - g.Expect(cfBuild.Status.Droplet.Registry.ImagePullSecrets).To(ConsistOf(corev1.LocalObjectReference{Name: "source-image-secret"})) + g.Expect(cfBuild.Status.Droplet.Registry.Image).To(Equal(imageRef)) + g.Expect(cfBuild.Status.Droplet.Registry.ImagePullSecrets).To(ConsistOf(corev1.LocalObjectReference{Name: imageSecret.Name})) }).Should(Succeed()) }) - It("fetches the image config", func() { - Expect(imageConfigGetter.ConfigCallCount()).NotTo(BeZero()) - _, creds, imageRef := imageConfigGetter.ConfigArgsForCall(imageConfigGetter.ConfigCallCount() - 1) - Expect(imageRef).To(Equal("some/image")) - Expect(creds).To(Equal(image.Creds{ - Namespace: cfSpace.Status.GUID, - SecretNames: []string{"source-image-secret"}, - })) - }) - Describe("privileged images", func() { succeededCondition := func(g Gomega) metav1.Condition { g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfBuild), cfBuild)).To(Succeed()) @@ -167,7 +177,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { When("the user is not specified", func() { BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{}, nil) + imageConfig.Config.User = "" }) It("fails the build", func() { @@ -177,7 +187,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { When("the user is 'root'", func() { BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{User: "root"}, nil) + imageConfig.Config.User = "root" }) It("fails the build", func() { @@ -187,7 +197,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { When("the user is '0'", func() { BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{User: "0"}, nil) + imageConfig.Config.User = "0" }) It("fails the build", func() { @@ -197,7 +207,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { When("the user is 'root:rootgroup'", func() { BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{User: "root:rootgroup"}, nil) + imageConfig.Config.User = "root:rootgroup" }) It("fails the build", func() { @@ -207,7 +217,7 @@ var _ = Describe("CFDockerBuildReconciler Integration Tests", func() { When("the user is '0:rootgroup'", func() { BeforeEach(func() { - imageConfigGetter.ConfigReturns(image.Config{User: "0:rootgroup"}, nil) + imageConfig.Config.User = "0:rootgroup" }) It("fails the build", func() { diff --git a/controllers/controllers/workloads/fake/image_config_getter.go b/controllers/controllers/workloads/fake/image_config_getter.go deleted file mode 100644 index c4707db12..000000000 --- a/controllers/controllers/workloads/fake/image_config_getter.go +++ /dev/null @@ -1,122 +0,0 @@ -// Code generated by counterfeiter. DO NOT EDIT. -package fake - -import ( - "context" - "sync" - - "code.cloudfoundry.org/korifi/controllers/controllers/workloads" - "code.cloudfoundry.org/korifi/tools/image" -) - -type ImageConfigGetter struct { - ConfigStub func(context.Context, image.Creds, string) (image.Config, error) - configMutex sync.RWMutex - configArgsForCall []struct { - arg1 context.Context - arg2 image.Creds - arg3 string - } - configReturns struct { - result1 image.Config - result2 error - } - configReturnsOnCall map[int]struct { - result1 image.Config - result2 error - } - invocations map[string][][]interface{} - invocationsMutex sync.RWMutex -} - -func (fake *ImageConfigGetter) Config(arg1 context.Context, arg2 image.Creds, arg3 string) (image.Config, error) { - fake.configMutex.Lock() - ret, specificReturn := fake.configReturnsOnCall[len(fake.configArgsForCall)] - fake.configArgsForCall = append(fake.configArgsForCall, struct { - arg1 context.Context - arg2 image.Creds - arg3 string - }{arg1, arg2, arg3}) - stub := fake.ConfigStub - fakeReturns := fake.configReturns - fake.recordInvocation("Config", []interface{}{arg1, arg2, arg3}) - fake.configMutex.Unlock() - if stub != nil { - return stub(arg1, arg2, arg3) - } - if specificReturn { - return ret.result1, ret.result2 - } - return fakeReturns.result1, fakeReturns.result2 -} - -func (fake *ImageConfigGetter) ConfigCallCount() int { - fake.configMutex.RLock() - defer fake.configMutex.RUnlock() - return len(fake.configArgsForCall) -} - -func (fake *ImageConfigGetter) ConfigCalls(stub func(context.Context, image.Creds, string) (image.Config, error)) { - fake.configMutex.Lock() - defer fake.configMutex.Unlock() - fake.ConfigStub = stub -} - -func (fake *ImageConfigGetter) ConfigArgsForCall(i int) (context.Context, image.Creds, string) { - fake.configMutex.RLock() - defer fake.configMutex.RUnlock() - argsForCall := fake.configArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 -} - -func (fake *ImageConfigGetter) ConfigReturns(result1 image.Config, result2 error) { - fake.configMutex.Lock() - defer fake.configMutex.Unlock() - fake.ConfigStub = nil - fake.configReturns = struct { - result1 image.Config - result2 error - }{result1, result2} -} - -func (fake *ImageConfigGetter) ConfigReturnsOnCall(i int, result1 image.Config, result2 error) { - fake.configMutex.Lock() - defer fake.configMutex.Unlock() - fake.ConfigStub = nil - if fake.configReturnsOnCall == nil { - fake.configReturnsOnCall = make(map[int]struct { - result1 image.Config - result2 error - }) - } - fake.configReturnsOnCall[i] = struct { - result1 image.Config - result2 error - }{result1, result2} -} - -func (fake *ImageConfigGetter) Invocations() map[string][][]interface{} { - fake.invocationsMutex.RLock() - defer fake.invocationsMutex.RUnlock() - fake.configMutex.RLock() - defer fake.configMutex.RUnlock() - copiedInvocations := map[string][][]interface{}{} - for key, value := range fake.invocations { - copiedInvocations[key] = value - } - return copiedInvocations -} - -func (fake *ImageConfigGetter) recordInvocation(key string, args []interface{}) { - fake.invocationsMutex.Lock() - defer fake.invocationsMutex.Unlock() - if fake.invocations == nil { - fake.invocations = map[string][][]interface{}{} - } - if fake.invocations[key] == nil { - fake.invocations[key] = [][]interface{}{} - } - fake.invocations[key] = append(fake.invocations[key], args) -} - -var _ workloads.ImageConfigGetter = new(ImageConfigGetter) diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go index fdf383920..783adf1ff 100644 --- a/controllers/controllers/workloads/suite_test.go +++ b/controllers/controllers/workloads/suite_test.go @@ -24,7 +24,9 @@ import ( "code.cloudfoundry.org/korifi/controllers/webhooks/version" "code.cloudfoundry.org/korifi/controllers/webhooks/workloads" "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tests/helpers/oci" "code.cloudfoundry.org/korifi/tools" + "code.cloudfoundry.org/korifi/tools/image" "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/ginkgo/v2" @@ -35,6 +37,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" admission "k8s.io/pod-security-admission/api" ctrl "sigs.k8s.io/controller-runtime" @@ -57,9 +60,10 @@ var ( imageRegistrySecret2 *corev1.Secret imageDeleter *fake.ImageDeleter packageCleaner *fake.PackageCleaner - imageConfigGetter *fake.ImageConfigGetter eventRecorder *controllerfake.EventRecorder buildCleaner *buildfake.BuildCleaner + imageClient image.Client + containerRegistry *oci.Registry ) const ( @@ -84,6 +88,8 @@ var _ = BeforeSuite(func() { ctx = context.Background() + containerRegistry = oci.NewContainerRegistry("user", "password") + testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("..", "..", "..", "helm", "korifi", "controllers", "crds"), @@ -121,6 +127,10 @@ var _ = BeforeSuite(func() { SpaceFinalizerAppDeletionTimeout: tools.PtrTo(int64(2)), } + k8sClient, err := k8sclient.NewForConfig(k8sManager.GetConfig()) + Expect(err).NotTo(HaveOccurred()) + imageClient = image.NewClient(k8sClient) + eventRecorder = new(controllerfake.EventRecorder) err = (NewCFAppReconciler( @@ -144,11 +154,10 @@ var _ = BeforeSuite(func() { err = (cfBuildpackBuildReconciler).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) - imageConfigGetter = new(fake.ImageConfigGetter) cfDockerBuildReconciler := NewCFDockerBuildReconciler( k8sManager.GetClient(), buildCleaner, - imageConfigGetter, + imageClient, k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFDockerBuild"), ) diff --git a/go.mod b/go.mod index 0f8dc9d60..237eedd19 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/buildpacks/pack v0.30.0 github.com/cloudfoundry/cf-test-helpers v1.0.1-0.20220603211108-d498b915ef74 github.com/distribution/distribution/v3 v3.0.0-20230223072852-e5d5810851d1 + github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c github.com/go-chi/chi v4.1.2+incompatible github.com/go-logr/logr v1.2.4 github.com/go-resty/resty/v2 v2.7.0 @@ -44,6 +45,7 @@ require ( ) require ( + github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/vmware-labs/reconciler-runtime v0.12.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect diff --git a/go.sum b/go.sum index 7039dba44..29fd6ca62 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 h1:rs0kDBt2zF4/CM9rO5/iH+U22jnTygPlqWgX55Ufcxg= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= @@ -175,6 +177,8 @@ github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBD github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c h1:DBGU7zCwrrPPDsD6+gqKG8UfMxenWg9BOJE/Nmfph+4= +github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c/go.mod h1:SHawtolbB0ZOFoRWgDwakX5WpwuIWAK88bUXVZqK0Ss= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -470,6 +474,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/tests/helpers/oci/registry.go b/tests/helpers/oci/registry.go new file mode 100644 index 000000000..2cf758071 --- /dev/null +++ b/tests/helpers/oci/registry.go @@ -0,0 +1,121 @@ +package oci + +import ( + "context" + "fmt" + "net/http/httptest" + "net/url" + "os" + + "github.com/distribution/distribution/v3/configuration" + dcontext "github.com/distribution/distribution/v3/context" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" + "github.com/distribution/distribution/v3/registry/handlers" + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" + "github.com/foomo/htpasswd" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + . "github.com/onsi/ginkgo/v2" //lint:ignore ST1001 this is a test file + . "github.com/onsi/gomega" //lint:ignore ST1001 this is a test file + "github.com/sirupsen/logrus" +) + +func init() { + logger := logrus.New() + logger.SetLevel(logrus.DebugLevel) + logger.SetOutput(GinkgoWriter) + dcontext.SetDefaultLogger(logrus.NewEntry(logger)) +} + +type Registry struct { + server *httptest.Server + username string + password string +} + +func (r *Registry) URL() string { + return r.server.URL +} + +func (r *Registry) ImageRef(relativeImageRef string) string { + serverURL, err := url.Parse(r.URL()) + Expect(err).NotTo(HaveOccurred()) + return fmt.Sprintf("%s/%s", serverURL.Host, relativeImageRef) +} + +func (r *Registry) PushImage(repoRef string, imageConfig *v1.ConfigFile) { + image, err := mutate.ConfigFile(empty.Image, imageConfig) + Expect(err).NotTo(HaveOccurred()) + + ref, err := name.ParseReference(repoRef) + Expect(err).NotTo(HaveOccurred()) + + pushOpts := []remote.Option{} + if r.username != "" && r.password != "" { + pushOpts = append(pushOpts, remote.WithAuth(&authn.Basic{ + Username: r.username, + Password: r.password, + })) + } + Expect(remote.Write(ref, image, pushOpts...)).To(Succeed()) +} + +func NewContainerRegistry(username, password string) *Registry { + htpasswdFile := generateHtpasswdFile(username, password) + + registry := &Registry{ + server: httptest.NewServer(handlers.NewApp(context.Background(), &configuration.Configuration{ + Auth: configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "Registry Realm", + "path": htpasswdFile, + }, + }, + Storage: configuration.Storage{ + "inmemory": configuration.Parameters{}, + "delete": configuration.Parameters{"enabled": true}, + }, + Loglevel: "debug", + })), + username: username, + password: password, + } + + DeferCleanup(func() { + registry.server.Close() + Expect(os.RemoveAll(htpasswdFile)).To(Succeed()) + }) + + return registry +} + +func NewNoAuthContainerRegistry() *Registry { + registry := &Registry{ + server: httptest.NewServer(handlers.NewApp(context.Background(), &configuration.Configuration{ + Storage: configuration.Storage{ + "inmemory": configuration.Parameters{}, + "delete": configuration.Parameters{"enabled": true}, + }, + Loglevel: "debug", + })), + } + + DeferCleanup(func() { + registry.server.Close() + }) + + return registry +} + +func generateHtpasswdFile(username, password string) string { + htpasswdFile, err := os.CreateTemp("", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(htpasswd.SetPassword(htpasswdFile.Name(), username, password, htpasswd.HashBCrypt)).To(Succeed()) + + return htpasswdFile.Name() +} diff --git a/api/repositories/dockercfg/dockercfg_suite_test.go b/tools/dockercfg/dockercfg_suite_test.go similarity index 100% rename from api/repositories/dockercfg/dockercfg_suite_test.go rename to tools/dockercfg/dockercfg_suite_test.go diff --git a/tools/dockercfg/secret.go b/tools/dockercfg/secret.go new file mode 100644 index 000000000..46ab30e8c --- /dev/null +++ b/tools/dockercfg/secret.go @@ -0,0 +1,71 @@ +package dockercfg + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func CreateDockerConfigSecret( + secretNamespace string, + secretName string, + dockerConfigs ...DockerServerConfig, +) (*corev1.Secret, error) { + dockerCfg, err := generateDockerCfgSecretData(dockerConfigs...) + if err != nil { + return nil, fmt.Errorf("failed to generate docker config secret data: %w", err) + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: secretNamespace, + Name: secretName, + }, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: dockerCfg, + }, + Type: corev1.SecretTypeDockerConfigJson, + }, nil +} + +type dockerConfigJSON struct { + Auths map[string]dockerConfigEntry `json:"auths" datapolicy:"token"` +} + +type dockerConfigEntry struct { + Auth string `json:"auth,omitempty"` +} + +type DockerServerConfig struct { + Server string + Username string + Password string +} + +func generateDockerCfgSecretData(entries ...DockerServerConfig) ([]byte, error) { + result := dockerConfigJSON{ + Auths: map[string]dockerConfigEntry{}, + } + + for _, config := range entries { + server := config.Server + if server == "" || server == "index.docker.io" { + server = "https://index.docker.io/v1/" + } + + result.Auths[server] = dockerConfigEntry{ + Auth: encodeDockerConfigFieldAuth(config.Username, config.Password), + } + + } + + return json.Marshal(result) +} + +func encodeDockerConfigFieldAuth(username, password string) string { + fieldValue := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(fieldValue)) +} diff --git a/tools/dockercfg/secret_test.go b/tools/dockercfg/secret_test.go new file mode 100644 index 000000000..445abfcab --- /dev/null +++ b/tools/dockercfg/secret_test.go @@ -0,0 +1,84 @@ +package dockercfg_test + +import ( + "encoding/base64" + + . "code.cloudfoundry.org/korifi/tests/matchers" + "code.cloudfoundry.org/korifi/tools/dockercfg" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("DockerConfigSecret", func() { + var ( + repos []dockercfg.DockerServerConfig + secret *corev1.Secret + ) + + BeforeEach(func() { + repos = []dockercfg.DockerServerConfig{{ + Server: "myrepo", + Username: "bob", + Password: "password", + }} + }) + + JustBeforeEach(func() { + var err error + secret, err = dockercfg.CreateDockerConfigSecret("secret-ns", "secret-name", repos...) + Expect(err).NotTo(HaveOccurred()) + }) + + It("creates a valid docker config secret", func() { + Expect(secret.Namespace).To(Equal("secret-ns")) + Expect(secret.Name).To(Equal("secret-name")) + Expect(secret.Type).To(Equal(corev1.SecretTypeDockerConfigJson)) + + Expect(secret.Data).To(HaveKeyWithValue( + corev1.DockerConfigJsonKey, + MatchJSONPath("$.auths.myrepo.auth", base64.StdEncoding.EncodeToString([]byte("bob:password"))), + )) + }) + When("the server is not set", func() { + BeforeEach(func() { + repos[0].Server = "" + }) + + It("sets the server to https://index.docker.io/v1/", func() { + Expect(secret.Data).To(HaveKeyWithValue( + corev1.DockerConfigJsonKey, + MatchJSONPath(`$.auths["https://index.docker.io/v1/"].auth`, base64.StdEncoding.EncodeToString([]byte("bob:password"))), + )) + }) + }) + + When("the server is index.docker.io", func() { + BeforeEach(func() { + repos[0].Server = "index.docker.io" + }) + + It("sets the server to https://index.docker.io/v1/", func() { + Expect(secret.Data).To(HaveKeyWithValue( + corev1.DockerConfigJsonKey, + MatchJSONPath(`$.auths["https://index.docker.io/v1/"].auth`, base64.StdEncoding.EncodeToString([]byte("bob:password"))), + )) + }) + }) + + When("multiple registries are passed", func() { + BeforeEach(func() { + repos = append(repos, dockercfg.DockerServerConfig{Server: "myotherserver"}) + }) + + It("adds entries for all servers", func() { + Expect(secret.Data).To(HaveKeyWithValue( + corev1.DockerConfigJsonKey, + SatisfyAll( + MatchJSONPath("$.auths.myrepo.auth", Not(BeEmpty())), + MatchJSONPath("$.auths.myotherserver.auth", Not(BeEmpty())), + )), + ) + }) + }) +}) diff --git a/tools/image/client_test.go b/tools/image/client_test.go index 72945a3e3..ee4f1e41d 100644 --- a/tools/image/client_test.go +++ b/tools/image/client_test.go @@ -2,16 +2,11 @@ package image_test import ( "os" - "strings" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "code.cloudfoundry.org/korifi/tests/helpers/oci" "code.cloudfoundry.org/korifi/tools/image" - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -22,6 +17,7 @@ var _ = Describe("Client", func() { otherZipFile *os.File testErr error pushRef string + imgCfg *v1.ConfigFile imgRef string creds image.Creds ) @@ -35,7 +31,19 @@ var _ = Describe("Client", func() { otherZipFile, err = os.Open("fixtures/anotherLayer.zip") Expect(err).NotTo(HaveOccurred()) - pushRef = strings.Replace(authRegistryServer.URL+"/foo/bar", "http://", "", 1) + pushRef = containerRegistry.ImageRef("foo/bar") + imgCfg = &v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{ + "foo": "bar", + }, + ExposedPorts: map[string]struct{}{ + "123": {}, + "456": {}, + }, + User: "my-user", + }, + } imgClient = image.NewClient(k8sClientset) creds = image.Creds{ @@ -107,21 +115,16 @@ var _ = Describe("Client", func() { }) }) - When("simulating ECR", func() { + When("seret name is empty (simulating ECR)", func() { BeforeEach(func() { - pushRef = strings.Replace(noAuthRegistryServer.URL+"/foo/bar", "http://", "", 1) + ecrRegistry := oci.NewNoAuthContainerRegistry() + pushRef = ecrRegistry.ImageRef("foo/bar") + creds.SecretNames = []string{} }) - When("the secret name is empty", func() { - BeforeEach(func() { - imgClient = image.NewClient(k8sClientset) - }) - - It("succeeds", func() { - Expect(testErr).NotTo(HaveOccurred()) - - Expect(imgRef).To(HavePrefix(pushRef)) - }) + It("succeeds", func() { + Expect(testErr).NotTo(HaveOccurred()) + Expect(imgRef).To(HavePrefix(pushRef)) }) }) }) @@ -131,7 +134,7 @@ var _ = Describe("Client", func() { BeforeEach(func() { pushRef += "/with/labels" - pushImgWithLabelsAndPorts(pushRef, map[string]string{"foo": "bar"}, []string{"123", "456"}) + containerRegistry.PushImage(pushRef, imgCfg) }) JustBeforeEach(func() { @@ -176,28 +179,27 @@ var _ = Describe("Client", func() { }) }) - When("simulating ECR", func() { + When("secret name is empty (simulating ECR)", func() { BeforeEach(func() { - pushRef = strings.Replace(noAuthRegistryServer.URL+"/foo/bar/with/labels", "http://", "", 1) - pushImgWithLabelsAndPorts(pushRef, map[string]string{"foo": "bar"}, []string{"123", "456"}) + ecrRegistry := oci.NewNoAuthContainerRegistry() + pushRef = ecrRegistry.ImageRef("foo/bar/with/labels") + ecrRegistry.PushImage(pushRef, imgCfg) + creds.SecretNames = []string{} }) - When("the secret name is empty", func() { - BeforeEach(func() { - imgClient = image.NewClient(k8sClientset) - }) - - It("succeeds", func() { - Expect(testErr).NotTo(HaveOccurred()) - Expect(config.Labels).To(Equal(map[string]string{"foo": "bar"})) - Expect(config.ExposedPorts).To(ConsistOf(int32(123), int32(456))) - }) + It("succeeds", func() { + Expect(testErr).NotTo(HaveOccurred()) + Expect(config.Labels).To(Equal(map[string]string{"foo": "bar"})) + Expect(config.ExposedPorts).To(ConsistOf(int32(123), int32(456))) }) }) When("ports are in the format 'port/protocol'", func() { BeforeEach(func() { - pushImgWithLabelsAndPorts(pushRef, map[string]string{"foo": "bar"}, []string{"123/protocol"}) + imgCfg.Config.ExposedPorts = map[string]struct{}{ + "123/protocol": {}, + } + containerRegistry.PushImage(pushRef, imgCfg) }) It("succeeds", func() { @@ -280,9 +282,10 @@ var _ = Describe("Client", func() { }) }) - When("simulating ECR", func() { + When("the secret name is empty (simulating ECR)", func() { BeforeEach(func() { - pushRef = strings.Replace(noAuthRegistryServer.URL+"/foo/bar", "http://", "", 1) + ecrRegistry := oci.NewNoAuthContainerRegistry() + pushRef = ecrRegistry.ImageRef("foo/bar") _, err := zipFile.Seek(0, 0) Expect(err).NotTo(HaveOccurred()) @@ -291,17 +294,11 @@ var _ = Describe("Client", func() { Expect(err).NotTo(HaveOccurred()) }) - When("the secret name is empty", func() { - BeforeEach(func() { - imgClient = image.NewClient(k8sClientset) - }) - - It("succeeds", func() { - Expect(testErr).NotTo(HaveOccurred()) + It("succeeds", func() { + Expect(testErr).NotTo(HaveOccurred()) - _, err := imgClient.Config(ctx, creds, imgRef) - Expect(err).To(MatchError(ContainSubstring("MANIFEST_UNKNOWN"))) - }) + _, err := imgClient.Config(ctx, creds, imgRef) + Expect(err).To(MatchError(ContainSubstring("MANIFEST_UNKNOWN"))) }) }) }) @@ -378,27 +375,3 @@ var _ = Describe("Client", func() { }) } }) - -func pushImgWithLabelsAndPorts(repoRef string, labels map[string]string, ports []string) { - portsMap := map[string]struct{}{} - for _, port := range ports { - portsMap[port] = struct{}{} - } - - image, err := mutate.ConfigFile(empty.Image, &v1.ConfigFile{ - Config: v1.Config{ - Labels: labels, - ExposedPorts: portsMap, - User: "my-user", - }, - }) - Expect(err).NotTo(HaveOccurred()) - - ref, err := name.ParseReference(repoRef) - Expect(err).NotTo(HaveOccurred()) - - Expect(remote.Write(ref, image, remote.WithAuth(&authn.Basic{ - Username: "user", - Password: "password", - }))).To(Succeed()) -} diff --git a/tools/image/fixtures/htpasswd b/tools/image/fixtures/htpasswd deleted file mode 100644 index f4244d449..000000000 --- a/tools/image/fixtures/htpasswd +++ /dev/null @@ -1 +0,0 @@ -user:$2y$05$Qr.7eBN67gIqfcd/tDA/aOXHwnpoxfzO8Vo/Ah/X54H8JrIJJdIf2 diff --git a/tools/image/image_suite_test.go b/tools/image/image_suite_test.go index d656520eb..95ce75524 100644 --- a/tools/image/image_suite_test.go +++ b/tools/image/image_suite_test.go @@ -2,22 +2,15 @@ package image_test import ( "context" - "encoding/base64" - "encoding/json" - "net/http/httptest" "os" "testing" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "code.cloudfoundry.org/korifi/tests/helpers/oci" + "code.cloudfoundry.org/korifi/tools/dockercfg" "code.cloudfoundry.org/korifi/tools/image" - "github.com/distribution/distribution/v3/configuration" - dcontext "github.com/distribution/distribution/v3/context" - _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" - "github.com/distribution/distribution/v3/registry/handlers" - _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,17 +36,16 @@ func TestImage(t *testing.T) { } var ( - imgClient image.Client - k8sClient client.Client - k8sClientset *kubernetes.Clientset - k8sConfig *rest.Config - authRegistryServer *httptest.Server - noAuthRegistryServer *httptest.Server - testEnv *envtest.Environment - ctx context.Context - registries []registry - secretName string - serviceAccountName string + imgClient image.Client + k8sClient client.Client + k8sClientset *kubernetes.Clientset + k8sConfig *rest.Config + containerRegistry *oci.Registry + testEnv *envtest.Environment + ctx context.Context + registries []registry + secretName string + serviceAccountName string ) type registry struct { @@ -75,67 +67,34 @@ var _ = BeforeSuite(func() { k8sConfig, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) - logger := logrus.New() - logger.SetLevel(logrus.DebugLevel) - logger.SetOutput(GinkgoWriter) - dcontext.SetDefaultLogger(logrus.NewEntry(logger)) - k8sClient, err = client.NewWithWatch(k8sConfig, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) k8sClientset, err = kubernetes.NewForConfig(k8sConfig) Expect(err).NotTo(HaveOccurred()) - authRegistryServer = httptest.NewServer(handlers.NewApp(ctx, &configuration.Configuration{ - Auth: configuration.Auth{ - "htpasswd": configuration.Parameters{ - "realm": "Registry Realm", - "path": "fixtures/htpasswd", // user:password - }, - }, - Storage: configuration.Storage{ - "inmemory": configuration.Parameters{}, - "delete": configuration.Parameters{"enabled": true}, - }, - Loglevel: "debug", - })) - - noAuthRegistryServer = httptest.NewServer(handlers.NewApp(ctx, &configuration.Configuration{ - Storage: configuration.Storage{ - "inmemory": configuration.Parameters{}, - "delete": configuration.Parameters{"enabled": true}, - }, - Loglevel: "debug", - })) + containerRegistry = oci.NewContainerRegistry("user", "password") secretName = testutils.GenerateGUID() - dockerConfig := map[string]map[string]any{"auths": {}} + + dockerConfigs := []dockercfg.DockerServerConfig{} for _, reg := range registries { - dockerConfig["auths"][reg.Server] = map[string]string{ - "username": reg.Username, - "password": reg.Password, - "auth": base64.StdEncoding.EncodeToString([]byte(reg.Username + ":" + reg.Password)), - } - } - dockerConfig["auths"][authRegistryServer.URL] = map[string]string{ - "username": "user", - "password": "password", - "auth": base64.StdEncoding.EncodeToString([]byte("user:password")), + dockerConfigs = append(dockerConfigs, dockercfg.DockerServerConfig{ + Server: reg.Server, + Username: reg.Username, + Password: reg.Password, + }) } + dockerConfigs = append(dockerConfigs, dockercfg.DockerServerConfig{ + Server: containerRegistry.URL(), + Username: "user", + Password: "password", + }) - dockerConfigJSON, err := json.Marshal(dockerConfig) + dockerSecret, err := dockercfg.CreateDockerConfigSecret("default", secretName, dockerConfigs...) Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, dockerSecret)).To(Succeed()) - Expect(k8sClient.Create(ctx, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: "default", - }, - Data: map[string][]byte{ - ".dockerconfigjson": dockerConfigJSON, - }, - Type: "kubernetes.io/dockerconfigjson", - })).To(Succeed()) Eventually(func(g Gomega) { _, getErr := k8sClientset.CoreV1().Secrets("default").Get(ctx, secretName, metav1.GetOptions{}) g.Expect(getErr).NotTo(HaveOccurred()) @@ -152,14 +111,6 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - if authRegistryServer != nil { - authRegistryServer.Close() - } - - if noAuthRegistryServer != nil { - noAuthRegistryServer.Close() - } - if k8sConfig != nil { Expect(testEnv.Stop()).To(Succeed()) }