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)
|
||||
case "org.osbuild.yum.config":
|
||||
options = new(YumConfigStageOptions)
|
||||
case "org.osbuild.yum.repos":
|
||||
options = new(YumReposStageOptions)
|
||||
default:
|
||||
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":{}}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
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 {
|
||||
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