osbuild2: support org.osbuild.yum.repos stage
Add support for the new `org.osbuild.yum.repos` stage for creating DNF / YUM repository configuration files. Add appropriate new unit tests for the stage implementation and modify necessary existing unit tests. Related to https://github.com/osbuild/osbuild/pull/932 Signed-off-by: Tomas Hozza <thozza@redhat.com>
This commit is contained in:
parent
37a39743bc
commit
97ef7fbf28
4 changed files with 312 additions and 0 deletions
|
|
@ -154,6 +154,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error {
|
||||||
options = new(PwqualityConfStageOptions)
|
options = new(PwqualityConfStageOptions)
|
||||||
case "org.osbuild.yum.config":
|
case "org.osbuild.yum.config":
|
||||||
options = new(YumConfigStageOptions)
|
options = new(YumConfigStageOptions)
|
||||||
|
case "org.osbuild.yum.repos":
|
||||||
|
options = new(YumReposStageOptions)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected stage type: %s", rawStage.Type)
|
return fmt.Errorf("unexpected stage type: %s", rawStage.Type)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -728,6 +728,24 @@ func TestStage_UnmarshalJSON(t *testing.T) {
|
||||||
data: []byte(`{"type":"org.osbuild.yum.config","options":{}}`),
|
data: []byte(`{"type":"org.osbuild.yum.config","options":{}}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "yum.repos",
|
||||||
|
fields: fields{
|
||||||
|
Type: "org.osbuild.yum.repos",
|
||||||
|
Options: &YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "my-repo",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
data: []byte(`{"type":"org.osbuild.yum.repos","options":{"filename":"test.repo","repos":[{"id":"my-repo","baseurl":["http://example.org/repo"]}]}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for idx, tt := range tests {
|
for idx, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|
|
||||||
106
internal/osbuild2/yum_repos_stage.go
Normal file
106
internal/osbuild2/yum_repos_stage.go
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package osbuild2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const repoFilenameRegex = "^[\\w.-]{1,250}\\.repo$"
|
||||||
|
const repoIDRegex = "^[\\w.\\-:]+$"
|
||||||
|
|
||||||
|
// YumRepository represents a single DNF / YUM repository.
|
||||||
|
type YumRepository struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
BaseURL []string `json:"baseurl,omitempty"`
|
||||||
|
Cost *int `json:"cost,omitempty"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
|
GPGKey []string `json:"gpgkey,omitempty"`
|
||||||
|
Metalink string `json:"metalink,omitempty"`
|
||||||
|
Mirrorlist string `json:"mirrorlist,omitempty"`
|
||||||
|
ModuleHotfixes *bool `json:"module_hotfixes,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
GPGCheck *bool `json:"gpgcheck,omitempty"`
|
||||||
|
RepoGPGCheck *bool `json:"repo_gpgcheck,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r YumRepository) validate() error {
|
||||||
|
// Plain string values which can not be empty strings as mandated by
|
||||||
|
// the stage schema are not validated. The reason is that if they
|
||||||
|
// are empty, they will be omitted from the resulting JSON, therefore
|
||||||
|
// they will be never passed to osbuild in the stage options.
|
||||||
|
// The same logic is applied to slices of strings, which must have
|
||||||
|
// at least one item if defined. These won't appear in the resulting
|
||||||
|
// JSON if the slice it empty.
|
||||||
|
|
||||||
|
idRegex := regexp.MustCompile(repoIDRegex)
|
||||||
|
if !idRegex.MatchString(r.Id) {
|
||||||
|
return fmt.Errorf("repo ID %q doesn't conform to schema (%s)", r.Id, repoIDRegex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// at least one of baseurl, metalink or mirrorlist must be provided
|
||||||
|
if len(r.BaseURL) == 0 && r.Metalink == "" && r.Mirrorlist == "" {
|
||||||
|
return fmt.Errorf("at least one of baseurl, metalink or mirrorlist values must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, url := range r.BaseURL {
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("baseurl must not be an empty string (idx %d)", idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, gpgkey := range r.GPGKey {
|
||||||
|
if gpgkey == "" {
|
||||||
|
return fmt.Errorf("gpgkey must not be an empty string (idx %d)", idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumReposStageOptions represents a single DNF / YUM repo configuration file.
|
||||||
|
type YumReposStageOptions struct {
|
||||||
|
// Filename of the configuration file to be created. Must end with '.repo'.
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
// List of repositories. The list must contain at least one item.
|
||||||
|
Repos []YumRepository `json:"repos"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumReposStageOptions) isStageOptions() {}
|
||||||
|
|
||||||
|
// NewYumReposStageOptions creates a new YumRepos Stage options object.
|
||||||
|
func NewYumReposStageOptions(filename string, repos []YumRepository) *YumReposStageOptions {
|
||||||
|
return &YumReposStageOptions{
|
||||||
|
Filename: filename,
|
||||||
|
Repos: repos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o YumReposStageOptions) validate() error {
|
||||||
|
filenameRegex := regexp.MustCompile(repoFilenameRegex)
|
||||||
|
if !filenameRegex.MatchString(o.Filename) {
|
||||||
|
return fmt.Errorf("filename %q doesn't conform to schema (%s)", o.Filename, repoFilenameRegex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(o.Repos) == 0 {
|
||||||
|
return fmt.Errorf("at least one repository must be defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, repo := range o.Repos {
|
||||||
|
if err := repo.validate(); err != nil {
|
||||||
|
return fmt.Errorf("validation of repository #%d failed: %s", idx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYumReposStage(options *YumReposStageOptions) *Stage {
|
||||||
|
if err := options.validate(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Stage{
|
||||||
|
Type: "org.osbuild.yum.repos",
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
186
internal/osbuild2/yum_repos_stage_test.go
Normal file
186
internal/osbuild2/yum_repos_stage_test.go
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
package osbuild2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/osbuild/osbuild-composer/internal/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewYumReposStage(t *testing.T) {
|
||||||
|
stageOptions := NewYumReposStageOptions("testing.repo", []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectedStage := &Stage{
|
||||||
|
Type: "org.osbuild.yum.repos",
|
||||||
|
Options: stageOptions,
|
||||||
|
}
|
||||||
|
actualStage := NewYumReposStage(stageOptions)
|
||||||
|
assert.Equal(t, expectedStage, actualStage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYumReposStageOptionsValidate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options YumReposStageOptions
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty-options",
|
||||||
|
options: YumReposStageOptions{},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-repos",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-filename",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "@#$%^&.rap",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-filename",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-baseurl-mirrorlist-metalink",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "baseurl-empty-string",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
BaseURL: []string{""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gpgkey-empty-string",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
GPGKey: []string{""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-repo-id",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "c@@l-id",
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "good-options-baseurl",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
Cost: common.IntToPtr(0),
|
||||||
|
Enabled: common.BoolToPtr(false),
|
||||||
|
ModuleHotfixes: common.BoolToPtr(false),
|
||||||
|
Name: "c@@l-name",
|
||||||
|
GPGCheck: common.BoolToPtr(true),
|
||||||
|
RepoGPGCheck: common.BoolToPtr(true),
|
||||||
|
BaseURL: []string{"http://example.org/repo"},
|
||||||
|
GPGKey: []string{"secretkey"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "good-options-mirrorlist",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
Cost: common.IntToPtr(200),
|
||||||
|
Enabled: common.BoolToPtr(true),
|
||||||
|
ModuleHotfixes: common.BoolToPtr(true),
|
||||||
|
Name: "c@@l-name",
|
||||||
|
GPGCheck: common.BoolToPtr(false),
|
||||||
|
RepoGPGCheck: common.BoolToPtr(false),
|
||||||
|
Mirrorlist: "http://example.org/mirrorlist",
|
||||||
|
GPGKey: []string{"secretkey"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "good-options-metalink",
|
||||||
|
options: YumReposStageOptions{
|
||||||
|
Filename: "test.repo",
|
||||||
|
Repos: []YumRepository{
|
||||||
|
{
|
||||||
|
Id: "cool-id",
|
||||||
|
Metalink: "http://example.org/metalink",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for idx, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.err {
|
||||||
|
assert.Errorf(t, tt.options.validate(), "%q didn't return an error [idx: %d]", tt.name, idx)
|
||||||
|
assert.Panics(t, func() { NewYumReposStage(&tt.options) })
|
||||||
|
} else {
|
||||||
|
assert.NoErrorf(t, tt.options.validate(), "%q returned an error [idx: %d]", tt.name, idx)
|
||||||
|
assert.NotPanics(t, func() { NewYumReposStage(&tt.options) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue