Repository: osbuild/osbuild-composer
Pull request #3405 (2023-04-24 09:22:28)

Enable custom repositories for cloudapi
This pull request enables custom repositories for cloudapi


<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->


kingsleyzissou: @thozza oh thanks! Early reviews are always great and appreciated

Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d739764d1..0861aab56 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -571,11 +571,6 @@ filesystem.sh:
   variables:
     SCRIPT: filesystem.sh
 
-custom-repos.sh:
-  extends: .integration
-  variables:
-    SCRIPT: custom-repos.sh
-
 cross-distro.sh:
   extends: .integration
   variables:
diff --git a/internal/blueprint/repository_customizations.go b/internal/blueprint/repository_customizations.go
index ceeef3bbd..652aca82d 100644
--- a/internal/blueprint/repository_customizations.go
+++ b/internal/blueprint/repository_customizations.go
@@ -3,6 +3,7 @@ package blueprint
 import (
 	"fmt"
 	"net/url"
+	"strings"
 
 	"github.com/osbuild/osbuild-composer/internal/fsnode"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
@@ -35,6 +36,23 @@ func validateCustomRepository(repo *RepositoryCustomization) error {
 	if repo.GPGCheck != nil && *repo.GPGCheck && len(repo.GPGKeys) == 0 {
 		return fmt.Errorf("Repository gpg check is set to true but no gpg keys are provided")
 	}
+
+	for _, key := range repo.GPGKeys {
+		// check for a valid GPG key prefix & contains GPG suffix
+		keyIsGPGKey := strings.HasPrefix(key, "-----BEGIN PGP PUBLIC KEY BLOCK-----") && strings.Contains(key, "-----END PGP PUBLIC KEY BLOCK-----")
+
+		// check for a valid URL
+		keyIsURL := false
+		_, err := url.ParseRequestURI(key)
+		if err == nil {
+			keyIsURL = true
+		}
+
+		if !keyIsGPGKey && !keyIsURL {
+			return fmt.Errorf("Repository gpg key is not a valid URL or a valid gpg key")
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go
index de5be01e2..be1bf6a9a 100644
--- a/internal/cloudapi/v2/handler.go
+++ b/internal/cloudapi/v2/handler.go
@@ -326,6 +326,64 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error {
 		}
 	}
 
+	if request.Customizations != nil && request.Customizations.CustomRepositories != nil {
+		repoCustomizations := []blueprint.RepositoryCustomization{}
+		for _, repo := range *request.Customizations.CustomRepositories {
+			repoCustomization := blueprint.RepositoryCustomization{
+				Id: repo.Id,
+			}
+
+			if repo.Name != nil {
+				repoCustomization.Name = *repo.Name
+			}
+
+			if repo.Filename != nil {
+				repoCustomization.Filename = *repo.Filename
+			}
+
+			if repo.Baseurl != nil && len(*repo.Baseurl) > 0 {
+				repoCustomization.BaseURLs = *repo.Baseurl
+			}
+
+			if repo.Gpgkey != nil && len(*repo.Gpgkey) > 0 {
+				repoCustomization.GPGKeys = *repo.Gpgkey
+			}
+
+			if repo.CheckGpg != nil {
+				repoCustomization.GPGCheck = repo.CheckGpg
+			}
+
+			if repo.RepoCheckGpg != nil {
+				repoCustomization.RepoGPGCheck = repo.RepoCheckGpg
+			}
+
+			if repo.Enabled != nil {
+				repoCustomization.Enabled = repo.Enabled
+			}
+
+			if repo.Metalink != nil {
+				repoCustomization.Metalink = *repo.Metalink
+			}
+
+			if repo.Mirrorlist != nil {
+				repoCustomization.Mirrorlist = *repo.Mirrorlist
+			}
+
+			if repo.SslVerify != nil {
+				repoCustomization.SSLVerify = *repo.SslVerify
+			}
+
+			repoCustomizations = append(repoCustomizations, repoCustomization)
+		}
+		if bp.Customizations == nil {
+			bp.Customizations = &blueprint.Customizations{
+				Repositories: repoCustomizations,
+			}
+		} else {
+			bp.Customizations.Repositories = repoCustomizations
+		}
+	}
+
 	// add the user-defined repositories only to the depsolve job for the
 	// payload (the packages for the final image)
 	var payloadRepositories []Repository
@@ -1362,6 +1420,13 @@ func genRepoConfig(repo Repository) (*rpmmd.RepoConfig, error) {
 		return nil, HTTPError(ErrorInvalidRepository)
 	}
 
+	if repo.Gpgkey != nil && *repo.Gpgkey != "" {
+		repoConfig.GPGKeys = []string{*repo.Gpgkey}
+	}
+	if repo.IgnoreSsl != nil {
+		repoConfig.IgnoreSSL = *repo.IgnoreSsl
+	}
+
 	if repo.CheckGpg != nil {
 		repoConfig.CheckGPG = repo.CheckGpg
 	}
diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go
index dd4ae4cbf..437b49a9a 100644
--- a/internal/cloudapi/v2/openapi.v2.gen.go
+++ b/internal/cloudapi/v2/openapi.v2.gen.go
@@ -293,13 +293,34 @@ type ContainerUploadStatus struct {
 	Url string `json:"url"`
 }
 
+// CustomRepository defines model for CustomRepository.
+type CustomRepository struct {
+	Baseurl      *[]string `json:"baseurl,omitempty"`
+	CheckGpg     *bool     `json:"check_gpg,omitempty"`
+	Enabled      *bool     `json:"enabled,omitempty"`
+	Filename     *string   `json:"filename,omitempty"`
+	Gpgkey       *[]string `json:"gpgkey,omitempty"`
+	Id           string    `json:"id"`
+	Metalink     *string   `json:"metalink,omitempty"`
+	Mirrorlist   *string   `json:"mirrorlist,omitempty"`
+	Name         *string   `json:"name,omitempty"`
+	RepoCheckGpg *bool     `json:"repo_check_gpg,omitempty"`
+	SslVerify    *bool     `json:"ssl_verify,omitempty"`
+}
+
 // Customizations defines model for Customizations.
 type Customizations struct {
-	Containers  *[]Container  `json:"containers,omitempty"`
-	Directories *[]Directory  `json:"directories,omitempty"`
-	Files       *[]File       `json:"files,omitempty"`
-	Filesystem  *[]Filesystem `json:"filesystem,omitempty"`
-	Packages    *[]string     `json:"packages,omitempty"`
+	Containers *[]Container `json:"containers,omitempty"`
+
+	// Extra repositories for packages specified in customizations. These
+	// repositories will be used to depsolve and retrieve packages. Additionally,
+	// these packages will be saved and imported to the `/etc/yum.repos.d/` directory
+	// on the image
+	CustomRepositories *[]CustomRepository `json:"custom_repositories,omitempty"`
+	Directories        *[]Directory        `json:"directories,omitempty"`
+	Files              *[]File             `json:"files,omitempty"`
+	Filesystem         *[]Filesystem       `json:"filesystem,omitempty"`
+	Packages           *[]string           `json:"packages,omitempty"`
 
 	// Extra repositories for packages specified in customizations. These
 	// repositories will only be used to depsolve and retrieve packages
@@ -855,129 +876,132 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
+	"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",
+	"cL59//8BAAD//wGZi7xIegAA",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/internal/cloudapi/v2/openapi.v2.yml b/internal/cloudapi/v2/openapi.v2.yml
index 001ece171..5755a3dd0 100644
--- a/internal/cloudapi/v2/openapi.v2.yml
+++ b/internal/cloudapi/v2/openapi.v2.yml
@@ -822,6 +822,43 @@ components:
           description: |
             Naming package sets for a repository assigns it to a specific part
             (pipeline) of the build process.
+    CustomRepository:
+      type: object
+      required:
+        - id
+      properties:
+        id:
+          type: string
+        name:
+          type: string
+        filename:
+          type: string
+        baseurl:
+          type: array
+          items:
+            type: string
+            format: uri
+            example: 'https://cdn.redhat.com/content/dist/rhel8/8/x86_64/baseos/os/'
+        mirrorlist:
+          type: string
+          format: uri
+          example: 'http://mirrorlist.centos.org/?release=8-stream&arch=aarch64&repo=BaseOS'
+        metalink:
+          type: string
+          format: uri
+          example: 'https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=x86_64'
+        enabled:
+          type: boolean
+        gpgkey:
+          type: array
+          items:
+            type: string
+        check_gpg:
+          type: boolean
+        repo_check_gpg:
+          type: boolean
+        ssl_verify:
+          type: boolean
     UploadOptions:
       anyOf:
       - $ref: '#/components/schemas/AWSEC2UploadOptions'
@@ -1015,6 +1052,15 @@ components:
             for the OS itself (they will not be available for the build root or
             any other part of the build process). The package_sets field for these
             repositories is ignored.
+        custom_repositories:
+          type: array
+          items:
+            $ref: '#/components/schemas/CustomRepository'
+          description: |
+            Extra repositories for packages specified in customizations. These
+            repositories will be used to depsolve and retrieve packages. Additionally,
+            these packages will be saved and imported to the `/etc/yum.repos.d/` directory
+            on the image
         filesystem:
           type: array
           items:
diff --git a/internal/dnfjson/dnfjson.go b/internal/dnfjson/dnfjson.go
index f9de0e9ac..5e884e22a 100644
--- a/internal/dnfjson/dnfjson.go
+++ b/internal/dnfjson/dnfjson.go
@@ -268,6 +268,7 @@ func (s *Solver) reposFromRPMMD(rpmRepos []rpmmd.RepoConfig) ([]repoConfig, erro
 			GPGKeys:        rr.GPGKeys,
 			IgnoreSSL:      rr.IgnoreSSL,
 			MetadataExpire: rr.MetadataExpire,
+			repoHash:       rr.Hash(),
 		}
 
 		if rr.CheckGPG != nil {
@@ -311,25 +312,15 @@ type repoConfig struct {
 	SSLClientKey   string   `json:"sslclientkey,omitempty"`
 	SSLClientCert  string   `json:"sslclientcert,omitempty"`
 	MetadataExpire string   `json:"metadata_expire,omitempty"`
+	// set the repo hass from `rpmmd.RepoConfig.Hash()` function
+	// rather than re-calculating it
+	repoHash string
 }
 
-// Hash calculates an ID string that uniquely represents a repository
-// configuration.  The Name and ImageTypeTags fields are not considered in the
-// calculation.
-// Copied from rpmmd/repository.go
+// use the hash calculated by the `rpmmd.RepoConfig.Hash()`
+// function rather than re-implementing the same code
 func (r *repoConfig) Hash() string {
-	bts := func(b bool) string {
-		return fmt.Sprintf("%T", b)
-	}
-	ats := func(s []string) string {
-		return strings.Join(s, "")
-	}
-	return fmt.Sprintf("%x", sha256.Sum256([]byte(ats(r.BaseURLs)+
-		r.Metalink+
-		r.MirrorList+
-		ats(r.GPGKeys)+
-		bts(r.IgnoreSSL)+
-		r.MetadataExpire)))
+	return r.repoHash
 }
 
 // Helper function for creating a depsolve request payload.


Test patch:
diff --git a/internal/blueprint/repository_customizations_test.go b/internal/blueprint/repository_customizations_test.go
index a827edf25..1c59f8c6e 100644
--- a/internal/blueprint/repository_customizations_test.go
+++ b/internal/blueprint/repository_customizations_test.go
@@ -65,6 +65,20 @@ func TestGetCustomRepositories(t *testing.T) {
 			},
 			wantErr: fmt.Errorf("Repository gpg check is set to true but no gpg keys are provided"),
 		},
+		{
+			name: "Test invalid GPG keys error",
+			expectedCustomizations: Customizations{
+				Repositories: []RepositoryCustomization{
+					{
+						Id:       "example-1",
+						BaseURLs: []string{"http://example-1.com"},
+						GPGKeys:  []string{"invalid"},
+						GPGCheck: common.ToPtr(true),
+					},
+				},
+			},
+			wantErr: fmt.Errorf("Repository gpg key is not a valid URL or a valid gpg key"),
+		},
 	}
 
 	for _, tt := range testCases {
@@ -245,14 +259,14 @@ func TestCustomRepoToRepoConfigAndGPGKeys(t *testing.T) {
 				{
 					Id:        "example-1",
 					BaseURLs:  []string{"http://example-1.com"},
-					GPGKeys:   []string{"fake-gpg-key-1"},
+					GPGKeys:   []string{"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n"},
 					GPGCheck:  common.ToPtr(true),
 					SSLVerify: true,
 				},
 				{
 					Id:        "example-2",
 					BaseURLs:  []string{"http://example-2.com"},
-					GPGKeys:   []string{"fake-gpg-key-2"},
+					GPGKeys:   []string{"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-2-----END PGP PUBLIC KEY BLOCK-----\n"},
 					GPGCheck:  common.ToPtr(true),
 					SSLVerify: true,
 				},
@@ -276,24 +290,30 @@ func TestCustomRepoToRepoConfigAndGPGKeys(t *testing.T) {
 				},
 			},
 			WantGPGKeys: []*fsnode.File{
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-0", nil, nil, nil, []byte("fake-gpg-key-1"))),
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-0", nil, nil, nil, []byte("fake-gpg-key-1"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-0", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-0", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n"))),
 			},
 		},
 		{
 			Name: "Test multiple inline gpgkeys",
 			Repos: []RepositoryCustomization{
 				{
-					Id:        "example-1",
-					BaseURLs:  []string{"http://example-1.com"},
-					GPGKeys:   []string{"fake-gpg-key-1", "fake-gpg-key-2"},
+					Id:       "example-1",
+					BaseURLs: []string{"http://example-1.com"},
+					GPGKeys: []string{
+						"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n",
+						"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-2-----END PGP PUBLIC KEY BLOCK-----\n",
+					},
 					GPGCheck:  common.ToPtr(true),
 					SSLVerify: true,
 				},
 				{
-					Id:        "example-2",
-					BaseURLs:  []string{"http://example-2.com"},
-					GPGKeys:   []string{"fake-gpg-key-1", "fake-gpg-key-2"},
+					Id:       "example-2",
+					BaseURLs: []string{"http://example-2.com"},
+					GPGKeys: []string{
+						"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n",
+						"-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-2-----END PGP PUBLIC KEY BLOCK-----\n",
+					},
 					GPGCheck:  common.ToPtr(true),
 					SSLVerify: true,
 				},
@@ -317,10 +337,10 @@ func TestCustomRepoToRepoConfigAndGPGKeys(t *testing.T) {
 				},
 			},
 			WantGPGKeys: []*fsnode.File{
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-0", nil, nil, nil, []byte("fake-gpg-key-1"))),
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-1", nil, nil, nil, []byte("fake-gpg-key-2"))),
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-0", nil, nil, nil, []byte("fake-gpg-key-1"))),
-				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-1", nil, nil, nil, []byte("fake-gpg-key-2"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-0", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-1-1", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-2-----END PGP PUBLIC KEY BLOCK-----\n"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-0", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-1-----END PGP PUBLIC KEY BLOCK-----\n"))),
+				ensureFileCreation(fsnode.NewFile("/etc/pki/rpm-gpg/RPM-GPG-KEY-example-2-1", nil, nil, nil, []byte("-----BEGIN PGP PUBLIC KEY BLOCK-----fake-gpg-key-2-----END PGP PUBLIC KEY BLOCK-----\n"))),
 			},
 		},
 	}
diff --git a/internal/cloudapi/v2/v2_internal_test.go b/internal/cloudapi/v2/v2_internal_test.go
index 59e4e925e..c3e84b576 100644
--- a/internal/cloudapi/v2/v2_internal_test.go
+++ b/internal/cloudapi/v2/v2_internal_test.go
@@ -77,10 +77,11 @@ func TestCollectRepos(t *testing.T) {
 	payloadPkgSets := []string{"blueprint"}
 
 	repos, err := convertRepos(irRepos, customRepos, payloadPkgSets)
+	assert.NoError(err)
 
 	// check lengths
 	assert.NoError(err)
-	assert.Equal(repos, expectedRepos)
+	assert.Equal(expectedRepos, repos)
 }
 
 func TestRepoConfigConversion(t *testing.T) {
diff --git a/internal/cloudapi/v2/v2_test.go b/internal/cloudapi/v2/v2_test.go
index 7c0c903d6..a19a40e87 100644
--- a/internal/cloudapi/v2/v2_test.go
+++ b/internal/cloudapi/v2/v2_test.go
@@ -920,6 +920,14 @@ func TestComposeCustomizations(t *testing.T) {
 				"ignore_ssl": false,
 				"gpg_key": "some-gpg-key"
 			}],
+			"custom_repositories": [{
+				"name": "hello", 
+				"id": "hello", 
+				"baseurl": [ "http://hello.com" ],
+				"gpg_key": [ "somekey" ],
+				"check_gpg": true,
+				"enabled": true
+			}],
 			"services": {
 				"enabled": [
 					"nftables"
diff --git a/internal/dnfjson/dnfjson_test.go b/internal/dnfjson/dnfjson_test.go
index c6c194b3c..8a585d9d7 100644
--- a/internal/dnfjson/dnfjson_test.go
+++ b/internal/dnfjson/dnfjson_test.go
@@ -115,11 +115,13 @@ func TestMakeDepsolveRequest(t *testing.T) {
 					ID:       baseOS.Hash(),
 					Name:     "baseos",
 					BaseURLs: []string{"https://example.org/baseos"},
+					repoHash: "b6b5cb4aae14a9ccc5f925876857b5cbfd757713a7c60e4c287502db80d8ecae",
 				},
 				{
 					ID:       appstream.Hash(),
 					Name:     "appstream",
 					BaseURLs: []string{"https://example.org/appstream"},
+					repoHash: "de4ae5f6f8288c8e6c745cac22bf3d923d3c738cc68c4b6046acac9a5eb3b8da",
 				},
 			},
 		},
@@ -152,16 +154,19 @@ func TestMakeDepsolveRequest(t *testing.T) {
 					ID:       baseOS.Hash(),
 					Name:     "baseos",
 					BaseURLs: []string{"https://example.org/baseos"},
+					repoHash: "b6b5cb4aae14a9ccc5f925876857b5cbfd757713a7c60e4c287502db80d8ecae",
 				},
 				{
 					ID:       appstream.Hash(),
 					Name:     "appstream",
 					BaseURLs: []string{"https://example.org/appstream"},
+					repoHash: "de4ae5f6f8288c8e6c745cac22bf3d923d3c738cc68c4b6046acac9a5eb3b8da",
 				},
 				{
 					ID:       userRepo.Hash(),
 					Name:     "user-repo",
 					BaseURLs: []string{"https://example.org/user-repo"},
+					repoHash: "127d1ae749d1b673dd8701871483ae93e82bff052ef2b9bf0b668ed1aca89f76",
 				},
 			},
 		},
@@ -194,11 +199,13 @@ func TestMakeDepsolveRequest(t *testing.T) {
 					ID:       baseOS.Hash(),
 					Name:     "baseos",
 					BaseURLs: []string{"https://example.org/baseos"},
+					repoHash: "b6b5cb4aae14a9ccc5f925876857b5cbfd757713a7c60e4c287502db80d8ecae",
 				},
 				{
 					ID:       appstream.Hash(),
 					Name:     "appstream",
 					BaseURLs: []string{"https://example.org/appstream"},
+					repoHash: "de4ae5f6f8288c8e6c745cac22bf3d923d3c738cc68c4b6046acac9a5eb3b8da",
 				},
 			},
 		},
@@ -239,16 +246,19 @@ func TestMakeDepsolveRequest(t *testing.T) {
 					ID:       baseOS.Hash(),
 					Name:     "baseos",
 					BaseURLs: []string{"https://example.org/baseos"},
+					repoHash: "b6b5cb4aae14a9ccc5f925876857b5cbfd757713a7c60e4c287502db80d8ecae",
 				},
 				{
 					ID:       appstream.Hash(),
 					Name:     "appstream",
 					BaseURLs: []string{"https://example.org/appstream"},
+					repoHash: "de4ae5f6f8288c8e6c745cac22bf3d923d3c738cc68c4b6046acac9a5eb3b8da",
 				},
 				{
 					ID:       userRepo.Hash(),
 					Name:     "user-repo",
 					BaseURLs: []string{"https://example.org/user-repo"},
+					repoHash: "127d1ae749d1b673dd8701871483ae93e82bff052ef2b9bf0b668ed1aca89f76",
 				},
 			},
 		},
@@ -290,21 +300,25 @@ func TestMakeDepsolveRequest(t *testing.T) {
 					ID:       baseOS.Hash(),
 					Name:     "baseos",
 					BaseURLs: []string{"https://example.org/baseos"},
+					repoHash: "b6b5cb4aae14a9ccc5f925876857b5cbfd757713a7c60e4c287502db80d8ecae",
 				},
 				{
 					ID:       appstream.Hash(),
 					Name:     "appstream",
 					BaseURLs: []string{"https://example.org/appstream"},
+					repoHash: "de4ae5f6f8288c8e6c745cac22bf3d923d3c738cc68c4b6046acac9a5eb3b8da",
 				},
 				{
 					ID:       userRepo.Hash(),
 					Name:     "user-repo",
 					BaseURLs: []string{"https://example.org/user-repo"},
+					repoHash: "127d1ae749d1b673dd8701871483ae93e82bff052ef2b9bf0b668ed1aca89f76",
 				},
 				{
 					ID:       userRepo2.Hash(),
 					Name:     "user-repo-2",
 					BaseURLs: []string{"https://example.org/user-repo-2"},
+					repoHash: "ca314d924e273218585cd60d7a5ffd91574df7ddf5b0574b31cf558971466170",
 				},
 			},
 		},
@@ -551,18 +565,28 @@ func TestErrorRepoInfo(t *testing.T) {
 }
 
 func TestRepoConfigHash(t *testing.T) {
-	rc := repoConfig{
-		ID:        "repoid-1",
-		Name:      "A test repository",
-		BaseURLs:  []string{"https://arepourl/"},
-		IgnoreSSL: false,
+	repos := []rpmmd.RepoConfig{
+		{
+			Id:        "repoid-1",
+			Name:      "A test repository",
+			BaseURLs:  []string{"https://arepourl/"},
+			IgnoreSSL: false,
+		},
+		{
+			BaseURLs: []string{"https://adifferenturl/"},
+		},
 	}
 
-	hash := rc.Hash()
+	solver := NewSolver("f36", "36", "x86_64", "fedora-36", "/tmp/cache")
+	solver.SetDNFJSONPath("../../dnf-json")
+
+	rcs, err := solver.reposFromRPMMD(repos)
+	assert.Nil(t, err)
+
+	hash := rcs[0].Hash()
 	assert.Equal(t, 64, len(hash))
 
-	rc.BaseURLs = []string{"https://adifferenturl/"}
-	assert.NotEqual(t, hash, rc.Hash())
+	assert.NotEqual(t, hash, rcs[1].Hash())
 }
 
 func TestRequestHash(t *testing.T) {
diff --git a/test/cases/api.sh b/test/cases/api.sh
index 7ea18b4f8..6419a0b53 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -367,6 +367,33 @@ EOF
 esac
 export DIR_FILES_CUSTOMIZATION_BLOCK
 
+# Define the customizations for the images here to not have to repeat them
+# in every image-type specific file.
+case "${IMAGE_TYPE}" in
+  # The Directories and Files customization is not supported for this image type.
+  "$IMAGE_TYPE_EDGE_INSTALLER")
+    CUSTOM_GPG_KEY=
+    REPOSITORY_CUSTOMIZATION_BLOCK=
+    ;;
+  *)
+    CUSTOM_GPG_KEY="-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGiBGRBSJURBACzCoe9UNfxOUiFLq9b60weSBFdr39mLViscecDWATNvXtgRoK/\nxl/4qpayzALRCQ2Ek/pMrbKPF/3ngECuBv7S+rI4n/rIia4FNcqzYeZAz4DE4NP/\neUGvz49tWhmH17hX/rmF9kz5kLq2bDZI4GDgZW/oMDdt2ivj092Ljm9jRwCgyQy3\nWEK6RJvIcSEh9vbdwVdMPOcD/iHqNejTMFwGyZfCWB0eIOoxUOUn/ZZpELTL2UpW\nGduCf3txb5SkK7M+WDbb0S5IvNXoi0tc13STiD6Oxg2O9PkSvvYb+8zxlhNoSTwy\n54j7Rf5FlnQ3TAFfjtQ5LCx56LKK73j4RjvKW//ktm5n54exsgo9Ry/e12T46dRg\n7tIlA/91rzLm57Qyc73A7zjgIzef9O6V5ZzowC+pp/jfb5pS9hXgROekLkMgX0vg\niA5rM5OpqK4bArVP1lRWnLyvghwO+TW763RVuXlS0scfzMy4g0NgrG6j7TIOKEqz\n4xQxOuwkudqiQr/kOqKuLxQBXa+5MJkyhfPmqYw5wpqyCwFa/7Q4b3NidWlsZCB0\nZXN0IChvc2J1aWxkIHRlc3QgZ3Bna2V5KSA8b3NidWlsZEBleGFtcGxlLmNvbT6I\newQTEQIAOxYhBGB8woiEPRKBO8Cr31lulpQgMejzBQJkQUiVAhsjBQsJCAcCAiIC\nBhUKCQgLAgQWAgMBAh4HAheAAAoJEFlulpQgMejzapMAoLmUg1mNDTRUaCrN/fzm\nHYLHL6jkAJ9pEKkJQiHB6SfD0fkiD2GkELYLubkBDQRkQUiVEAQAlAAXrQ572vuw\nxI3W8GSZmOQiAYOQmOKRloLEy6VZ3NSOb9y2TXj33QTkJBPOM17AzB7E+YjZrpUt\ngl6LlXmfjMcJAcXhFaUBCilAcMwMlLl7DtnSkLnLIXYmHiN0v83BH/H0EPutOc5l\n0QIyugutifp9SJz2+EWpC4bjA7GFkQ8AAwUD/1tLEGqCJ37O8gfzYt2PWkqBEoOY\n0Z3zwVS6PWW/IIkak9dAJ0iX5NMeFWpzFNfviDPHqhEdUR55zsxyUZIZlCX5jwmA\nt7qm3cbH4HNU1Ogq3Q9hykbTPWPZVkpvNm/TO8TA2brhkz3nuS8Hbmh+rjXFOSZj\nDQBUxItuuj2hhpQEiGAEGBECACAWIQRgfMKIhD0SgTvAq99ZbpaUIDHo8wUCZEFI\nlQIbDAAKCRBZbpaUIDHo83fQAKDHgFIaggaNsvDQkj7vMX0fecHRhACfS9Bvxn2W\nWSb6T+gChmYBseZwk/k=\n=DQ3i\n-----END PGP PUBLIC KEY BLOCK-----"
+    REPOSITORY_CUSTOMIZATION_BLOCK=$(cat <<EOF
+,
+    "custom_repositories": [{
+        "id": "example",
+        "name": "Example repo",
+        "baseurl": [ "http://example.com" ],
+        "gpgkey": [ "$CUSTOM_GPG_KEY" ],
+        "check_gpg": true,
+        "enabled": true
+    }]
+EOF
+)
+    ;;
+esac
+export CUSTOM_GPG_KEY
+export REPOSITORY_CUSTOMIZATION_BLOCK
+
 # generate a temp key for user tests
 ssh-keygen -t rsa-sha2-512 -f "${WORKDIR}/usertest" -C "usertest" -N ""
 
diff --git a/test/cases/api/aws.sh b/test/cases/api/aws.sh
index 4a44dbd08..b4fb5b9ba 100644
--- a/test/cases/api/aws.sh
+++ b/test/cases/api/aws.sh
@@ -63,7 +63,7 @@ function createReqFile() {
         "name": "user2",
         "key": "$(cat "${WORKDIR}/usertest.pub")"
       }
-    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}
+    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}${REPOSITORY_CUSTOMIZATION_BLOCK}
   },
   "image_request": {
       "architecture": "$ARCH",
diff --git a/test/cases/api/azure.sh b/test/cases/api/azure.sh
index d15592d40..269c95d94 100644
--- a/test/cases/api/azure.sh
+++ b/test/cases/api/azure.sh
@@ -84,7 +84,7 @@ function createReqFile() {
     "packages": [
       "postgresql",
       "dummy"
-    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}
+    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}${REPOSITORY_CUSTOMIZATION_BLOCK}
   },
   "image_request": {
     "architecture": "$ARCH",
diff --git a/test/cases/api/common/common.sh b/test/cases/api/common/common.sh
index 7bfa4e948..68cb14859 100644
--- a/test/cases/api/common/common.sh
+++ b/test/cases/api/common/common.sh
@@ -46,6 +46,8 @@ function _instanceCheck() {
 
   # Verify that directories and files customization worked as expected
   verify_dirs_files_customization "$_ssh"
+
+  verify_repository_customization "$_ssh"
 }
 
 WORKER_REFRESH_TOKEN_PATH="/etc/osbuild-worker/token"
@@ -129,3 +131,38 @@ function verify_dirs_files_customization {
     exit 1
   fi
 }
+
+# Verify that repository customizations worked as expected
+function verify_repository_customization {
+  echo "✔️ Checking custom repositories"
+  local _ssh="$1"
+  local _error=0
+
+  local _custom_repo_file="/etc/yum.repos.d/example.repo"
+  local _key_file_path="/etc/pki/rpm-gpg/RPM-GPG-KEY-example-0"
+
+  # verify that `/etc/yum.repos.d/example.repo` exists
+  # and contains path to gpg key file
+  local cust_repo_contains_key_path
+  cust_repo_contains_key_path=$($_ssh cat "$_custom_repo_file" | grep -c "${_key_file_path}")
+  if [[ "$cust_repo_contains_key_path" -le 0 ]]; then
+    echo "File $_custom_repo_file does not contain ${_key_file_path}}"
+    _error=1
+  fi
+
+  # verify that gpg key file has been saved to image
+  # and the contents match the expected gpg key
+  local local_key remote_key key_diff
+  local_key=$(echo -e "$CUSTOM_GPG_KEY")
+  remote_key=$($_ssh cat "${_key_file_path}")
+  key_diff=$(diff <(echo "$local_key") <(echo "$remote_key") | wc -l)
+  if [[ "$key_diff" -gt 0 ]]; then
+    echo "File $_key_file_path has wrong content"
+    _error=1
+  fi
+
+  if [[ "$_error" == "1" ]]; then
+    echo "Testing of custom repositories failed."
+    exit 1
+  fi
+}
diff --git a/test/cases/api/common/s3.sh b/test/cases/api/common/s3.sh
index f943da40d..f974e975e 100644
--- a/test/cases/api/common/s3.sh
+++ b/test/cases/api/common/s3.sh
@@ -78,7 +78,7 @@ function createReqFileGuest() {
         "name": "user2",
         "key": "$(cat "${WORKDIR}/usertest.pub")"
       }
-    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}
+    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}${REPOSITORY_CUSTOMIZATION_BLOCK}
   },
   "image_request": {
     "architecture": "$ARCH",
diff --git a/test/cases/api/gcp.sh b/test/cases/api/gcp.sh
index a0e0235ca..b9d7b6ab2 100644
--- a/test/cases/api/gcp.sh
+++ b/test/cases/api/gcp.sh
@@ -71,7 +71,7 @@ function createReqFile() {
     "packages": [
       "postgresql",
       "dummy"
-    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}
+    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}${REPOSITORY_CUSTOMIZATION_BLOCK}
   },
   "image_request": {
     "architecture": "$ARCH",
diff --git a/test/cases/custom-repos.sh b/test/cases/custom-repos.sh
deleted file mode 100755
index 484187f32..000000000
--- a/test/cases/custom-repos.sh
+++ /dev/null
@@ -1,203 +0,0 @@
-#!/usr/bin/bash
-
-#
-# Test the ability to specify custom repositories
-#
-set -euo pipefail
-
-source /etc/os-release
-
-# Provision the software under test.
-/usr/libexec/osbuild-composer-test/provision.sh none
-
-source /usr/libexec/osbuild-composer-test/set-env-variables.sh
-source /usr/libexec/tests/osbuild-composer/shared_lib.sh
-
-if ! nvrGreaterOrEqual "osbuild-composer" "81"; then
-    echo "SKIP: test not supported on osbuild-composer < 81"
-    exit 0
-fi
-
-# Set up variables.
-case "${ID}-${VERSION_ID}" in
-    fedora*)
-        ;;
-    rhel-*)
-        ;;
-    centos-*)
-        ;;
-    *)
-        echo "unsupported distro: ${ID}-${VERSION_ID}"
-        exit 1;;
-esac
-
-TEST_UUID=$(uuidgen)
-IMAGE_KEY="osbuild-composer-test-${TEST_UUID}"
-ARTIFACTS="ci-artifacts"
-mkdir -p "${ARTIFACTS}"
-
-# Set up temporary files.
-TEMPDIR=$(mktemp -d)
-BLUEPRINT_FILE=${TEMPDIR}/blueprint.toml
-COMPOSE_START=${TEMPDIR}/compose-start-${IMAGE_KEY}.json
-COMPOSE_INFO=${TEMPDIR}/compose-info-${IMAGE_KEY}.json
-
-# Workaround the problem that 'image-info' can not read SELinux labels unknown to the host from the image
-OSBUILD_LABEL=$(matchpathcon -n "$(which osbuild)")
-sudo chcon "$OSBUILD_LABEL" /usr/libexec/osbuild-composer-test/image-info
-
-# Get the compose log.
-get_compose_log () {
-    COMPOSE_ID=$1
-    LOG_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.log
-
-    # Download the logs.
-    sudo composer-cli compose log "${COMPOSE_ID}" | tee "${LOG_FILE}" > /dev/null
-}
-
-# Get the compose metadata.
-get_compose_metadata () {
-    COMPOSE_ID=$1
-    METADATA_FILE=${ARTIFACTS}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.json
-
-    # Download the metadata.
-    sudo composer-cli compose metadata "${COMPOSE_ID}" > /dev/null
-
-    # Find the tarball and extract it.
-    TARBALL=$(basename "$(find . -maxdepth 1 -type f -name "*-metadata.tar")")
-    sudo tar -xf "${TARBALL}" -C "${TEMPDIR}"
-    sudo rm -f "${TARBALL}"
-
-    # Move the JSON file into place.
-    sudo cat "${TEMPDIR}"/"${COMPOSE_ID}".json | jq -M '.' | tee "${METADATA_FILE}" > /dev/null
-}
-
-# Build ostree image.
-build_image() {
-    blueprint_name=$1
-    image_type=$2
-
-    # Get worker unit file so we can watch the journal.
-    WORKER_UNIT=$(sudo systemctl list-units | grep -o -E "osbuild.*worker.*\.service")
-    sudo journalctl -af -n 1 -u "${WORKER_UNIT}" &
-    WORKER_JOURNAL_PID=$!
-    # Stop watching the worker journal when exiting.
-    trap 'sudo pkill -P ${WORKER_JOURNAL_PID}' EXIT
-
-    # Start the compose.
-    greenprint "🚀 Starting compose"
-    sudo composer-cli --json compose start "${blueprint_name}" "${image_type}" | tee "${COMPOSE_START}"
-    COMPOSE_ID=$(get_build_info ".build_id" "${COMPOSE_START}")
-
-    # Wait for the compose to finish.
-    greenprint "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
-    while true; do
-        sudo composer-cli --json compose info "${COMPOSE_ID}" | tee "${COMPOSE_INFO}" > /dev/null
-        COMPOSE_STATUS=$(get_build_info ".queue_status" "${COMPOSE_INFO}")
-
-        # Is the compose finished?
-        if [[ ${COMPOSE_STATUS} != RUNNING ]] && [[ ${COMPOSE_STATUS} != WAITING ]]; then
-            break
-        fi
-
-        # Wait 30 seconds and try again.
-        sleep 5
-    done
-
-    # Capture the compose logs from osbuild.
-    greenprint "💬 Getting compose log and metadata"
-    get_compose_log "${COMPOSE_ID}"
-    get_compose_metadata "${COMPOSE_ID}"
-
-    # Kill the journal monitor immediately and remove the trap
-    sudo pkill -P "${WORKER_JOURNAL_PID}"
-    trap - EXIT
-
-    # Did the compose finish with success?
-    if [[ ${COMPOSE_STATUS} != FINISHED ]]; then
-        echo "Something went wrong with the compose. 😢"
-        exit 1
-    fi
-}
-
-greenprint "🚀 Checking custom repositories"
-
-REPO_ID="example"
-REPO_NAME="Example repo"
-REPO_FILENAME="custom.repo"
-REPO_BASEURL="https://example.com/download/yum"
-REPO_GPGKEY_URL="https://example.com/example-key.asc"
-REPO_GPGKEY="-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGiBGRBSJURBACzCoe9UNfxOUiFLq9b60weSBFdr39mLViscecDWATNvXtgRoK/\nxl/4qpayzALRCQ2Ek/pMrbKPF/3ngECuBv7S+rI4n/rIia4FNcqzYeZAz4DE4NP/\neUGvz49tWhmH17hX/rmF9kz5kLq2bDZI4GDgZW/oMDdt2ivj092Ljm9jRwCgyQy3\nWEK6RJvIcSEh9vbdwVdMPOcD/iHqNejTMFwGyZfCWB0eIOoxUOUn/ZZpELTL2UpW\nGduCf3txb5SkK7M+WDbb0S5IvNXoi0tc13STiD6Oxg2O9PkSvvYb+8zxlhNoSTwy\n54j7Rf5FlnQ3TAFfjtQ5LCx56LKK73j4RjvKW//ktm5n54exsgo9Ry/e12T46dRg\n7tIlA/91rzLm57Qyc73A7zjgIzef9O6V5ZzowC+pp/jfb5pS9hXgROekLkMgX0vg\niA5rM5OpqK4bArVP1lRWnLyvghwO+TW763RVuXlS0scfzMy4g0NgrG6j7TIOKEqz\n4xQxOuwkudqiQr/kOqKuLxQBXa+5MJkyhfPmqYw5wpqyCwFa/7Q4b3NidWlsZCB0\nZXN0IChvc2J1aWxkIHRlc3QgZ3Bna2V5KSA8b3NidWlsZEBleGFtcGxlLmNvbT6I\newQTEQIAOxYhBGB8woiEPRKBO8Cr31lulpQgMejzBQJkQUiVAhsjBQsJCAcCAiIC\nBhUKCQgLAgQWAgMBAh4HAheAAAoJEFlulpQgMejzapMAoLmUg1mNDTRUaCrN/fzm\nHYLHL6jkAJ9pEKkJQiHB6SfD0fkiD2GkELYLubkBDQRkQUiVEAQAlAAXrQ572vuw\nxI3W8GSZmOQiAYOQmOKRloLEy6VZ3NSOb9y2TXj33QTkJBPOM17AzB7E+YjZrpUt\ngl6LlXmfjMcJAcXhFaUBCilAcMwMlLl7DtnSkLnLIXYmHiN0v83BH/H0EPutOc5l\n0QIyugutifp9SJz2+EWpC4bjA7GFkQ8AAwUD/1tLEGqCJ37O8gfzYt2PWkqBEoOY\n0Z3zwVS6PWW/IIkak9dAJ0iX5NMeFWpzFNfviDPHqhEdUR55zsxyUZIZlCX5jwmA\nt7qm3cbH4HNU1Ogq3Q9hykbTPWPZVkpvNm/TO8TA2brhkz3nuS8Hbmh+rjXFOSZj\nDQBUxItuuj2hhpQEiGAEGBECACAWIQRgfMKIhD0SgTvAq99ZbpaUIDHo8wUCZEFI\nlQIbDAAKCRBZbpaUIDHo83fQAKDHgFIaggaNsvDQkj7vMX0fecHRhACfS9Bvxn2W\nWSb6T+gChmYBseZwk/k=\n=DQ3i\n-----END PGP PUBLIC KEY BLOCK-----\n"
-REPO_GPGCHECK="true"
-REPO_ENABLED="true"
-
-# Write a basic blueprint for our image.
-tee "$BLUEPRINT_FILE" > /dev/null << EOF
-name = "custom-repo"
-description = "A base system with custom repositories enabled"
-version = "0.0.1"
-
-[[customizations.repositories]]
-id="${REPO_ID}"
-name="${REPO_NAME}"
-filename="${REPO_FILENAME}"
-baseurls=[ "${REPO_BASEURL}" ]
-gpgkeys=[ "${REPO_GPGKEY}", "${REPO_GPGKEY_URL}" ]
-gpgcheck=${REPO_GPGCHECK}
-enabled=${REPO_ENABLED}
-EOF
-
-# Prepare the blueprint for the compose.
-greenprint "📋 Preparing custom-repo blueprint"
-sudo composer-cli blueprints push "${BLUEPRINT_FILE}"
-sudo composer-cli blueprints depsolve custom-repo
-
-build_image custom-repo qcow2
-
-# Download the image
-greenprint "📥 Downloading the image"
-sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
-IMAGE_FILENAME="${COMPOSE_ID}-disk.qcow2"
-
-greenprint "💬 Getting image info"
-INFO="$(sudo /usr/libexec/osbuild-composer-test/image-info "${IMAGE_FILENAME}")"
-
-# Clean compose and blueprints.
-greenprint "🧼 Clean up osbuild-composer"
-sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
-sudo composer-cli blueprints delete custom-repo > /dev/null
-
-greenprint "📗 Checking results"
-CUSTOM_REPO_EXISTS=$(jq --arg r "custom.repo" 'any(.yum_repos[] | keys | .[] == $r; .)' <<< "${INFO}")
-echo "CUSTOM_REPO_EXISTS: ${CUSTOM_REPO_EXISTS}"
-if "${CUSTOM_REPO_EXISTS}"; then
-    greenprint "✅ Custom image-builder repo file has been created"
-else
-    echo "❌ Custom repo has not been created"
-    exit 1
-fi
-
-REPO_CONTAINS_PATH_TO_KEY=$(jq --arg r "$REPO_ID" --arg k "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-$REPO_ID-0" \
-  '.yum_repos[]."custom.repo" | to_entries | map(select(.key == $r) | .value) | any(.[] | .gpgkey | contains($k); .)' \
-  <<< "${INFO}")
-echo "REPO_CONTAINS_PATH_TO_KEY ${REPO_CONTAINS_PATH_TO_KEY}"
-if "${REPO_CONTAINS_PATH_TO_KEY}"; then
-    greenprint "✅ Custom image-builder repo file contains gpgkey file location"
-else
-    echo "❌ Custom repo does not contain gpgkey file location"
-    exit 1
-fi
-
-REPO_CONTAINS_KEY_URL=$(jq --arg r "$REPO_ID" --arg k "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-$REPO_ID-0" \
-  '.yum_repos[]."custom.repo" | to_entries | map(select(.key == $r) | .value) | any(.[] | .gpgkey | contains($k); .)' \
-  <<< "${INFO}")
-echo "REPO_CONTAINS_KEY_URL: ${REPO_CONTAINS_KEY_URL}"
-if "${REPO_CONTAINS_KEY_URL}"; then
-    greenprint "✅ Custom image-builder repo file contains gpgkey url"
-else
-    echo "❌ Custom repo does not contain gpgkey url"
-    exit 1
-fi
-
-echo "🎉 All tests passed."
-exit 0