diff --git a/pkg/osbuild/librepo_source.go b/pkg/osbuild/librepo_source.go new file mode 100644 index 0000000000..73ed129140 --- /dev/null +++ b/pkg/osbuild/librepo_source.go @@ -0,0 +1,126 @@ +package osbuild + +import ( + "fmt" + + "github.com/osbuild/images/pkg/rpmmd" +) + +// LibrepoSource wraps the org.osbuild.librepo osbuild source +type LibrepoSource struct { + Items map[string]*LibrepoSourceItem `json:"items"` + Options *LibrepoSourceOptions `json:"options"` +} + +func NewLibrepoSource() *LibrepoSource { + return &LibrepoSource{ + Items: make(map[string]*LibrepoSourceItem), + Options: &LibrepoSourceOptions{ + Mirrors: make(map[string]*LibrepoSourceMirror), + }, + } +} + +// AddPackage adds the given *depsolved* pkg to the downloading. It +// needs the *depsovled* repoConfig so that the repoID of the two can +// be matched up +func (source *LibrepoSource) AddPackage(pkg rpmmd.PackageSpec, repos []rpmmd.RepoConfig) error { + pkgRepo, err := findRepoById(repos, pkg.RepoID) + if err != nil { + return fmt.Errorf("cannot find repo-id for pkg %v: %v", pkg.Name, err) + } + if _, ok := source.Options.Mirrors[pkgRepo.Id]; !ok { + mirror, err := mirrorFromRepo(pkgRepo) + if err != nil { + return err + } + source.Options.Mirrors[pkgRepo.Id] = mirror + } + mirror := source.Options.Mirrors[pkgRepo.Id] + if pkg.IgnoreSSL { + mirror.Insecure = true + } + // this should never happen but we should still check to avoid + // potential security issues + if mirror.Insecure && !pkg.IgnoreSSL { + return fmt.Errorf("inconsistent SSL configuration: package %v requires SSL but mirror %v is configured to ignore SSL", pkg.Name, mirror.URL) + } + if pkg.Secrets == "org.osbuild.rhsm" { + mirror.Secrets = &URLSecrets{ + Name: "org.osbuild.rhsm", + } + } else if pkg.Secrets == "org.osbuild.mtls" { + mirror.Secrets = &URLSecrets{ + Name: "org.osbuild.mtls", + } + } + + item := &LibrepoSourceItem{ + Path: pkg.Path, + MirrorID: pkgRepo.Id, + } + source.Items[pkg.Checksum] = item + return nil +} + +func (LibrepoSource) isSource() {} + +type LibrepoSourceItem struct { + Path string `json:"path"` + MirrorID string `json:"mirror"` +} + +func findRepoById(repos []rpmmd.RepoConfig, repoID string) (*rpmmd.RepoConfig, error) { + type info struct { + ID string + Name string + } + var repoInfo []info + for _, repo := range repos { + repoInfo = append(repoInfo, info{repo.Id, repo.Name}) + if repo.Id == repoID { + return &repo, nil + } + } + + return nil, fmt.Errorf("cannot find repo-id %v in %+v", repoID, repoInfo) +} + +func mirrorFromRepo(repo *rpmmd.RepoConfig) (*LibrepoSourceMirror, error) { + switch { + case repo.Metalink != "": + return &LibrepoSourceMirror{ + URL: repo.Metalink, + Type: "metalink", + }, nil + case repo.MirrorList != "": + return &LibrepoSourceMirror{ + URL: repo.MirrorList, + Type: "mirrorlist", + }, nil + case len(repo.BaseURLs) > 0: + return &LibrepoSourceMirror{ + // XXX: should we pick a random one instead? + URL: repo.BaseURLs[0], + Type: "baseurl", + }, nil + } + + return nil, fmt.Errorf("cannot find metalink, mirrorlist or baseurl for %+v", repo) +} + +// librepoSourceOptions are the JSON options for source org.osbuild.librepo +type LibrepoSourceOptions struct { + Mirrors map[string]*LibrepoSourceMirror `json:"mirrors"` +} + +type LibrepoSourceMirror struct { + URL string `json:"url"` + Type string `json:"type"` + + Insecure bool `json:"insecure,omitempty"` + Secrets *URLSecrets `json:"secrets,omitempty"` + + MaxParallels *int `json:"max-parallels,omitempty"` + FastestMirror bool `json:"fastest-mirror,omitempty"` +} diff --git a/pkg/osbuild/librepo_source_test.go b/pkg/osbuild/librepo_source_test.go new file mode 100644 index 0000000000..7e7fe63de2 --- /dev/null +++ b/pkg/osbuild/librepo_source_test.go @@ -0,0 +1,238 @@ +package osbuild_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/rpmmd" +) + +var ( + opensslPkg = rpmmd.PackageSpec{ + Name: "openssl-libs", + Epoch: 1, + Version: "3.0.1", + Release: "5.el9", + Arch: "x86_64", + RemoteLocation: "https://example.com/repo/Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", + Checksum: "sha256:fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666", + Path: "Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", + RepoID: "repo_id_metalink", + } + + pamPkg = rpmmd.PackageSpec{ + Name: "pam", + Epoch: 0, + Version: "1.5.1", + Release: "9.el9", + Arch: "x86_64", + RemoteLocation: "https://example.com/repo/Packages/pam-1.5.1-9.el9.x86_64.rpm", + Checksum: "sha256:e64caedce811645ecdd78e7b4ae83c189aa884ff1ba6445374f39186c588c52c", + Path: "Packages/pam-1.5.1-9.el9.x86_64.rpm", + RepoID: "repo_id_mirrorlist", + } + + dbusPkg = rpmmd.PackageSpec{ + Name: "dbus", + Epoch: 1, + Version: "1.12.20", + Release: "5.el9", + Arch: "x86_64", + RemoteLocation: "https://example.com/repo/Packages/dbus-1.12.20-5.el9.x86_64.rpm", + Checksum: "sha256:bb85bd28cc162e98da53b756b988ffd9350f4dbcc186f4c6962ae047e27f83d3", + Path: "Packages/dbus-1.12.20-5.el9.x86_64.rpm", + RepoID: "repo_id_baseurls", + } +) + +var fakeRepos = []rpmmd.RepoConfig{ + { + Id: "repo_id_metalink", + Name: "repo1", + Metalink: "http://example.com/metalink", + }, + { + Id: "repo_id_mirrorlist", + Name: "repo1", + MirrorList: "http://example.com/mirrorlist", + }, + { + Id: "repo_id_baseurls", + Name: "repo1", + BaseURLs: []string{"http://example.com/baseurl1"}, + }, +} + +func TestLibrepoAddPackage(t *testing.T) { + sources := osbuild.NewLibrepoSource() + err := sources.AddPackage(opensslPkg, fakeRepos) + assert.NoError(t, err) + err = sources.AddPackage(pamPkg, fakeRepos) + assert.NoError(t, err) + err = sources.AddPackage(dbusPkg, fakeRepos) + assert.NoError(t, err) + + expectedJSON := `{ + "items": { + "sha256:bb85bd28cc162e98da53b756b988ffd9350f4dbcc186f4c6962ae047e27f83d3": { + "path": "Packages/dbus-1.12.20-5.el9.x86_64.rpm", + "mirror": "repo_id_baseurls" + }, + "sha256:e64caedce811645ecdd78e7b4ae83c189aa884ff1ba6445374f39186c588c52c": { + "path": "Packages/pam-1.5.1-9.el9.x86_64.rpm", + "mirror": "repo_id_mirrorlist" + }, + "sha256:fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666": { + "path": "Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", + "mirror": "repo_id_metalink" + } + }, + "options": { + "mirrors": { + "repo_id_baseurls": { + "url": "http://example.com/baseurl1", + "type": "baseurl" + }, + "repo_id_metalink": { + "url": "http://example.com/metalink", + "type": "metalink" + }, + "repo_id_mirrorlist": { + "url": "http://example.com/mirrorlist", + "type": "mirrorlist" + } + } + } +}` + b, err := json.MarshalIndent(sources, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) +} + +func TestLibrepoInsecure(t *testing.T) { + pkg := opensslPkg + pkg.IgnoreSSL = true + + sources := osbuild.NewLibrepoSource() + err := sources.AddPackage(pkg, fakeRepos) + assert.NoError(t, err) + + expectedJSON := `{ + "items": { + "sha256:fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666": { + "path": "Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", + "mirror": "repo_id_metalink" + } + }, + "options": { + "mirrors": { + "repo_id_metalink": { + "url": "http://example.com/metalink", + "type": "metalink", + "insecure": true + } + } + } +}` + b, err := json.MarshalIndent(sources, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) +} + +func TestLibrepoSecrets(t *testing.T) { + for _, secret := range []string{"org.osbuild.rhsm", "org.osbuild.mtls"} { + pkg := opensslPkg + pkg.Secrets = secret + + sources := osbuild.NewLibrepoSource() + err := sources.AddPackage(pkg, fakeRepos) + assert.NoError(t, err) + + expectedJSON := fmt.Sprintf(`{ + "items": { + "sha256:fcf2515ec9115551c99d552da721803ecbca23b7ae5a974309975000e8bef666": { + "path": "Packages/openssl-libs-3.0.1-5.el9.x86_64.rpm", + "mirror": "repo_id_metalink" + } + }, + "options": { + "mirrors": { + "repo_id_metalink": { + "url": "http://example.com/metalink", + "type": "metalink", + "secrets": { + "name": "%s" + } + } + } + } +}`, secret) + b, err := json.MarshalIndent(sources, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) + } +} + +func TestLibrepoJsonMinimal(t *testing.T) { + expectedJSON := `{ + "url": "http://example.com", + "type": "metalink" +}` + sourceMirror := osbuild.LibrepoSourceMirror{ + URL: "http://example.com", + Type: "metalink", + } + b, err := json.MarshalIndent(sourceMirror, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) +} + +func TestLibrepoJsonFull(t *testing.T) { + expectedJSON := `{ + "url": "http://example.com", + "type": "metalink", + "insecure": true, + "secrets": { + "name": "org.osbuild.mtls" + }, + "max-parallels": 10, + "fastest-mirror": true +}` + sourceMirror := osbuild.LibrepoSourceMirror{ + URL: "http://example.com", + Type: "metalink", + Insecure: true, + Secrets: &osbuild.URLSecrets{Name: "org.osbuild.mtls"}, + MaxParallels: common.ToPtr(10), + FastestMirror: true, + } + b, err := json.MarshalIndent(sourceMirror, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) +} + +func TestLibrepoRepoIdNotFound(t *testing.T) { + pkg := opensslPkg + pkg.RepoID = "invalid_repo_id" + + sources := osbuild.NewLibrepoSource() + err := sources.AddPackage(pkg, fakeRepos) + assert.EqualError(t, err, `cannot find repo-id for pkg openssl-libs: cannot find repo-id invalid_repo_id in [{ID:repo_id_metalink Name:repo1} {ID:repo_id_mirrorlist Name:repo1} {ID:repo_id_baseurls Name:repo1}]`) +} + +func TestLibrepoInconsistentSSLConfiguration(t *testing.T) { + pkg := opensslPkg + pkg.IgnoreSSL = true + + sources := osbuild.NewLibrepoSource() + err := sources.AddPackage(pkg, fakeRepos) + assert.NoError(t, err) + pkg.IgnoreSSL = false + err = sources.AddPackage(pkg, fakeRepos) + assert.EqualError(t, err, `inconsistent SSL configuration: package openssl-libs requires SSL but mirror http://example.com/metalink is configured to ignore SSL`) +}