blueprint: move pipeline generation into its own package

Introduce the `distro` package, which contains an interface for OS
implementations. Its main purpose is to convert a blueprint to a
distro-specific pipeline.

Also introduce the `distro/fedora30` package. It is the first
implementation of the distro interface. Most of its code has been copied
with minimal modifications from the blueprint package.

The `blueprint` package is now back to serving a single purpose:
representing a weldr blueprint. It does not depend on the `pipeline`
package anymore.

Change osbuild-composer and osbuild-pipeline to use the new API,
hard-coding "fedora-30". This looks a bit weird now, but is the same
behavior as before.

All test cases now also take an "distro" key in the "compose" object.
This commit is contained in:
Lars Karlitski 2019-11-06 01:01:39 +00:00 committed by Tom Gundersen
parent 3ae39e54c5
commit b33ed9e5d2
28 changed files with 661 additions and 541 deletions

View file

@ -7,6 +7,9 @@ import (
"os"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
)
func main() {
@ -27,7 +30,9 @@ func main() {
panic("Colud not parse blueprint")
}
}
pipeline, err := blueprint.ToPipeline(format)
f30 := distro.New("fedora-30")
pipeline, err := f30.Pipeline(blueprint, format)
if err != nil {
panic(err.Error())
}

View file

@ -1,13 +1,6 @@
// Package blueprint contains primitives for representing weldr blueprints and
// translating them to OSBuild pipelines
// Package blueprint contains primitives for representing weldr blueprints
package blueprint
import (
"sort"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
// An InvalidOutputFormatError is returned when a requested output format is
// not supported. The requested format is included as the error message.
type InvalidOutputFormatError struct {
@ -40,45 +33,7 @@ type Group struct {
Name string `json:"name"`
}
type output interface {
translate(b *Blueprint) (*pipeline.Pipeline, error)
getName() string
getMime() string
}
var outputs = map[string]output{
"ami": &amiOutput{},
"ext4-filesystem": &ext4Output{},
"live-iso": &liveIsoOutput{},
"partitioned-disk": &diskOutput{},
"qcow2": &qcow2Output{},
"openstack": &openstackOutput{},
"tar": &tarOutput{},
"vhd": &vhdOutput{},
"vmdk": &vmdkOutput{},
}
// ListOutputFormats returns a sorted list of the supported output formats
func ListOutputFormats() []string {
formats := make([]string, 0, len(outputs))
for name := range outputs {
formats = append(formats, name)
}
sort.Strings(formats)
return formats
}
// ToPipeline converts the blueprint to a pipeline for a given output format.
func (b *Blueprint) ToPipeline(outputFormat string) (*pipeline.Pipeline, error) {
if output, exists := outputs[outputFormat]; exists {
return output.translate(b)
}
return nil, &InvalidOutputFormatError{outputFormat}
}
func (b *Blueprint) getKernelCustomization() *KernelCustomization {
func (b *Blueprint) GetKernelCustomization() *KernelCustomization {
if b.Customizations == nil {
return nil
}
@ -86,21 +41,11 @@ func (b *Blueprint) getKernelCustomization() *KernelCustomization {
return b.Customizations.Kernel
}
// FilenameFromType gets the canonical filename and MIME type for a given
// output format
func FilenameFromType(outputFormat string) (string, string, error) {
if output, exists := outputs[outputFormat]; exists {
return output.getName(), output.getMime(), nil
}
return "", "", &InvalidOutputFormatError{outputFormat}
}
func (p Package) ToNameVersion() string {
// Omit version to prevent all packages with prefix of name to be installed
if p.Version == "*" {
return p.Name
}
// Omit version to prevent all packages with prefix of name to be installed
if p.Version == "*" {
return p.Name
}
return p.Name + "-" + p.Version
return p.Name + "-" + p.Version
}

View file

@ -1,13 +1,9 @@
package blueprint_test
import (
"encoding/json"
"io/ioutil"
"reflect"
"testing"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
func TestInvalidOutputFormatError_Error(t *testing.T) {
@ -29,167 +25,3 @@ func TestInvalidOutputFormatError_Error(t *testing.T) {
})
}
}
func TestListOutputFormats(t *testing.T) {
tests := []struct {
name string
want []string
}{
{
name: "basic",
want: []string{
"ami",
"ext4-filesystem",
"live-iso",
"openstack",
"partitioned-disk",
"qcow2",
"tar",
"vhd",
"vmdk",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := blueprint.ListOutputFormats(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ListOutputFormats() = %v, want %v", got, tt.want)
}
})
}
}
func TestBlueprint_ToPipeline(t *testing.T) {
pipelinePath := "../../test/cases/"
fileInfos, err := ioutil.ReadDir(pipelinePath)
if err != nil {
t.Errorf("Could not read pipelines directory '%s': %v", pipelinePath, err)
}
for _, fileInfo := range fileInfos {
type compose struct {
OutputFormat string `json:"output-format"`
Blueprint *blueprint.Blueprint `json:"blueprint"`
}
var tt struct {
Compose *compose `json:"compose"`
Pipeline *pipeline.Pipeline `json:"pipeline,omitempty"`
}
file, err := ioutil.ReadFile(pipelinePath + fileInfo.Name())
if err != nil {
t.Errorf("Colud not read test-case '%s': %v", fileInfo.Name(), err)
}
err = json.Unmarshal([]byte(file), &tt)
if err != nil {
t.Errorf("Colud not parse test-case '%s': %v", fileInfo.Name(), err)
}
if tt.Compose == nil || tt.Compose.Blueprint == nil {
t.Logf("Skipping '%s'.", fileInfo.Name())
continue
}
t.Run(tt.Compose.OutputFormat, func(t *testing.T) {
got, err := tt.Compose.Blueprint.ToPipeline(tt.Compose.OutputFormat)
if (err != nil) != (tt.Pipeline == nil) {
t.Errorf("Blueprint.ToPipeline() error = %v", err)
return
}
if tt.Pipeline != nil {
if !reflect.DeepEqual(got, tt.Pipeline) {
// Without this the "difference" is just a list of pointers.
gotJson, _ := json.Marshal(got)
fileJson, _ := json.Marshal(tt.Pipeline)
t.Errorf("Blueprint.ToPipeline() =\n%v,\nwant =\n%v", string(gotJson), string(fileJson))
}
}
})
}
}
func TestFilenameFromType(t *testing.T) {
type args struct {
outputFormat string
}
tests := []struct {
name string
args args
want string
want1 string
wantErr bool
}{
{
name: "ami",
args: args{"ami"},
want: "image.ami",
want1: "application/x-qemu-disk",
},
{
name: "ext4",
args: args{"ext4-filesystem"},
want: "filesystem.img",
want1: "application/octet-stream",
},
{
name: "live-iso",
args: args{"live-iso"},
want: "image.iso",
want1: "application/x-iso9660-image",
},
{
name: "openstack",
args: args{"openstack"},
want: "image.qcow2",
want1: "application/x-qemu-disk",
},
{
name: "partitioned-disk",
args: args{"partitioned-disk"},
want: "disk.img",
want1: "application/octet-stream",
},
{
name: "qcow2",
args: args{"qcow2"},
want: "image.qcow2",
want1: "application/x-qemu-disk",
},
{
name: "tar",
args: args{"tar"},
want: "root.tar.xz",
want1: "application/x-tar",
},
{
name: "vhd",
args: args{"vhd"},
want: "image.vhd",
want1: "application/x-vhd",
},
{
name: "vmdk",
args: args{"vmdk"},
want: "disk.vmdk",
want1: "application/x-vmdk",
},
{
name: "invalid-output-type",
args: args{"foobar"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := blueprint.FilenameFromType(tt.args.outputFormat)
if (err != nil) != tt.wantErr {
t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if got != tt.want {
t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
}
}
})
}
}

View file

@ -1,13 +1,5 @@
package blueprint
import (
"github.com/osbuild/osbuild-composer/internal/crypt"
"github.com/osbuild/osbuild-composer/internal/pipeline"
"log"
"strconv"
"strings"
)
type Customizations struct {
Hostname *string `json:"hostname,omitempty"`
Kernel *KernelCustomization `json:"kernel,omitempty"`
@ -72,260 +64,9 @@ type ServicesCustomization struct {
}
type CustomizationError struct {
message string
Message string
}
func (e *CustomizationError) Error() string {
return e.message
}
func (c *Customizations) customizeAll(p *pipeline.Pipeline) error {
c.customizeHostname(p)
c.customizeGroup(p)
if err := c.customizeUserAndSSHKey(p); err != nil {
return err
}
c.customizeTimezone(p)
c.customizeNTPServers(p)
c.customizeLanguages(p)
c.customizeKeyboard(p)
c.customizeFirewall(p)
c.customizeServices(p)
return nil
}
func (c *Customizations) customizeHostname(p *pipeline.Pipeline) {
if c.Hostname == nil {
return
}
p.AddStage(
pipeline.NewHostnameStage(
&pipeline.HostnameStageOptions{Hostname: *c.Hostname},
),
)
}
func (c *Customizations) customizeGroup(p *pipeline.Pipeline) {
if len(c.Group) == 0 {
return
}
groups := make(map[string]pipeline.GroupsStageOptionsGroup)
for _, group := range c.Group {
if userCustomizationsContainUsername(c.User, group.Name) {
log.Println("group with name ", group.Name, " was not created, because user with same name was defined!")
continue
}
groupData := pipeline.GroupsStageOptionsGroup{}
if group.GID != nil {
gid := strconv.Itoa(*group.GID)
groupData.GID = &gid
}
groups[group.Name] = groupData
}
p.AddStage(
pipeline.NewGroupsStage(
&pipeline.GroupsStageOptions{Groups: groups},
),
)
}
func (c *Customizations) assertAllUsersExistForSSHCustomizations() error {
for _, sshkey := range c.SSHKey {
userFound := false
for _, user := range c.User {
if user.Name == sshkey.User {
userFound = true
}
}
if !userFound {
return &CustomizationError{"Cannot set SSH key for non-existing user " + sshkey.User}
}
}
return nil
}
func (c *Customizations) customizeUserAndSSHKey(p *pipeline.Pipeline) error {
if len(c.User) == 0 {
if len(c.SSHKey) > 0 {
return &CustomizationError{"SSH key customization defined but no user customizations are defined"}
}
return nil
}
// return error if ssh key customization without user defined in user customization if found
if e := c.assertAllUsersExistForSSHCustomizations(); e != nil {
return e
}
users := make(map[string]pipeline.UsersStageOptionsUser)
for _, user := range c.User {
if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) {
cryptedPassword, err := crypt.CryptSHA512(*user.Password)
if err != nil {
return err
}
user.Password = &cryptedPassword
}
userData := pipeline.UsersStageOptionsUser{
Groups: user.Groups,
Description: user.Description,
Home: user.Home,
Shell: user.Shell,
Password: user.Password,
Key: user.Key,
}
if user.UID != nil {
uid := strconv.Itoa(*user.UID)
userData.UID = &uid
}
if user.GID != nil {
gid := strconv.Itoa(*user.GID)
userData.GID = &gid
}
// process sshkey customizations
if additionalKeys := findKeysForUser(c.SSHKey, user.Name); len(additionalKeys) > 0 {
joinedKeys := strings.Join(additionalKeys, "\n")
if userData.Key != nil {
*userData.Key += "\n" + joinedKeys
} else {
userData.Key = &joinedKeys
}
}
users[user.Name] = userData
}
p.AddStage(
pipeline.NewUsersStage(
&pipeline.UsersStageOptions{Users: users},
),
)
return nil
}
func (c *Customizations) customizeTimezone(p *pipeline.Pipeline) {
if c.Timezone == nil || c.Timezone.Timezone == nil {
return
}
// TODO: lorax (anaconda) automatically installs chrony if timeservers are defined
// except for the case when chrony is explicitly removed from installed packages (using -chrony)
// this is currently not supported, no checks whether chrony is installed are not performed
p.AddStage(
pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{
Zone: *c.Timezone.Timezone,
}),
)
}
func (c *Customizations) customizeNTPServers(p *pipeline.Pipeline) {
if c.Timezone == nil || len(c.Timezone.NTPServers) == 0 {
return
}
p.AddStage(
pipeline.NewChronyStage(&pipeline.ChronyStageOptions{
Timeservers: c.Timezone.NTPServers,
}),
)
}
func (c *Customizations) customizeLanguages(p *pipeline.Pipeline) {
if c.Locale == nil || len(c.Locale.Languages) == 0 {
return
}
// TODO: you can specify more languages in customization
// The first one is the primary one, we can set in the locale stage, this should currently work
// Also, ALL the listed languages are installed using langpack-* packages
// This is currently not implemented!
// See anaconda src: pyanaconda/payload/dnfpayload.py:772
p.AddStage(
pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{
Language: c.Locale.Languages[0],
}),
)
}
func (c *Customizations) customizeKeyboard(p *pipeline.Pipeline) {
if c.Locale == nil || c.Locale.Keyboard == nil {
return
}
p.AddStage(
pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{
Keymap: *c.Locale.Keyboard,
}),
)
}
func (c *Customizations) customizeFirewall(p *pipeline.Pipeline) {
if c.Firewall == nil {
return
}
var enabledServices, disabledServices []string
if c.Firewall.Services != nil {
enabledServices = c.Firewall.Services.Enabled
disabledServices = c.Firewall.Services.Disabled
}
p.AddStage(
pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{
Ports: c.Firewall.Ports,
EnabledServices: enabledServices,
DisabledServices: disabledServices,
}),
)
}
func (c *Customizations) customizeServices(p *pipeline.Pipeline) {
if c.Services == nil {
return
}
p.AddStage(
pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{
EnabledServices: c.Services.Enabled,
DisabledServices: c.Services.Disabled,
}),
)
}
func findKeysForUser(sshKeyCustomizations []SSHKeyCustomization, user string) (keys []string) {
for _, sshKey := range sshKeyCustomizations {
if sshKey.User == user {
keys = append(keys, sshKey.Key)
}
}
return
}
func userCustomizationsContainUsername(userCustomizations []UserCustomization, name string) bool {
for _, usr := range userCustomizations {
if usr.Name == name {
return true
}
}
return false
return e.Message
}

48
internal/distro/distro.go Normal file
View file

@ -0,0 +1,48 @@
package distro
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type Distro interface {
// Returns a sorted list of the output formats this distro supports.
ListOutputFormats() []string
// Returns the canonical filename and MIME type for a given output
// format
FilenameFromType(outputFormat string) (string, string, error)
// Returns an osbuild pipeline that generates an image in the given
// output format with all packages and customizations specified in the
// given blueprint. `outputFormat` must be one returned by
// ListOutputFormats().
Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error)
}
// An InvalidOutputFormatError is returned when a requested output format is
// not supported. The requested format is included as the error message.
type InvalidOutputFormatError struct {
Format string
}
func (e *InvalidOutputFormatError) Error() string {
return e.Format
}
var registered = map[string]Distro{}
func New(name string) Distro {
distro, ok := registered[name]
if !ok {
panic("unknown distro: " + name)
}
return distro
}
func Register(name string, distro Distro) {
if _, exists := registered[name]; exists {
panic("a distro with this name already exists: " + name)
}
registered[name] = distro
}

View file

@ -0,0 +1,66 @@
package distro_test
import (
"encoding/json"
"io/ioutil"
"reflect"
"testing"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/pipeline"
_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
)
func TestDistro_Pipeline(t *testing.T) {
pipelinePath := "../../test/cases/"
fileInfos, err := ioutil.ReadDir(pipelinePath)
if err != nil {
t.Errorf("Could not read pipelines directory '%s': %v", pipelinePath, err)
}
for _, fileInfo := range fileInfos {
type compose struct {
Distro string `json:"distro"`
OutputFormat string `json:"output-format"`
Blueprint *blueprint.Blueprint `json:"blueprint"`
}
var tt struct {
Compose *compose `json:"compose"`
Pipeline *pipeline.Pipeline `json:"pipeline,omitempty"`
}
file, err := ioutil.ReadFile(pipelinePath + fileInfo.Name())
if err != nil {
t.Errorf("Colud not read test-case '%s': %v", fileInfo.Name(), err)
}
err = json.Unmarshal([]byte(file), &tt)
if err != nil {
t.Errorf("Colud not parse test-case '%s': %v", fileInfo.Name(), err)
}
if tt.Compose == nil || tt.Compose.Blueprint == nil {
t.Logf("Skipping '%s'.", fileInfo.Name())
continue
}
t.Run(tt.Compose.OutputFormat, func(t *testing.T) {
d := distro.New(tt.Compose.Distro)
if d == nil {
t.Errorf("unknown distro: %v", tt.Compose.Distro)
return
}
got, err := d.Pipeline(tt.Compose.Blueprint, tt.Compose.OutputFormat)
if (err != nil) != (tt.Pipeline == nil) {
t.Errorf("distro.Pipeline() error = %v", err)
return
}
if tt.Pipeline != nil {
if !reflect.DeepEqual(got, tt.Pipeline) {
// Without this the "difference" is just a list of pointers.
gotJson, _ := json.Marshal(got)
fileJson, _ := json.Marshal(tt.Pipeline)
t.Errorf("d.Pipeline() =\n%v,\nwant =\n%v", string(gotJson), string(fileJson))
}
}
})
}
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type amiOutput struct{}
func (t *amiOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *amiOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"@Core",
"chrony",
@ -30,7 +33,7 @@ func (t *amiOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
addF30QemuAssembler(p, "qcow2", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,261 @@
package fedora30
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/crypt"
"github.com/osbuild/osbuild-composer/internal/pipeline"
"log"
"strconv"
"strings"
)
func customizeAll(p *pipeline.Pipeline, c *blueprint.Customizations) error {
customizeHostname(p, c)
customizeGroup(p, c)
if err := customizeUserAndSSHKey(p, c); err != nil {
return err
}
customizeTimezone(p, c)
customizeNTPServers(p, c)
customizeLanguages(p, c)
customizeKeyboard(p, c)
customizeFirewall(p, c)
customizeServices(p, c)
return nil
}
func customizeHostname(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Hostname == nil {
return
}
p.AddStage(
pipeline.NewHostnameStage(
&pipeline.HostnameStageOptions{Hostname: *c.Hostname},
),
)
}
func customizeGroup(p *pipeline.Pipeline, c *blueprint.Customizations) {
if len(c.Group) == 0 {
return
}
groups := make(map[string]pipeline.GroupsStageOptionsGroup)
for _, group := range c.Group {
if userCustomizationsContainUsername(c.User, group.Name) {
log.Println("group with name ", group.Name, " was not created, because user with same name was defined!")
continue
}
groupData := pipeline.GroupsStageOptionsGroup{}
if group.GID != nil {
gid := strconv.Itoa(*group.GID)
groupData.GID = &gid
}
groups[group.Name] = groupData
}
p.AddStage(
pipeline.NewGroupsStage(
&pipeline.GroupsStageOptions{Groups: groups},
),
)
}
func assertAllUsersExistForSSHCustomizations(c *blueprint.Customizations) error {
for _, sshkey := range c.SSHKey {
userFound := false
for _, user := range c.User {
if user.Name == sshkey.User {
userFound = true
}
}
if !userFound {
return &blueprint.CustomizationError{"Cannot set SSH key for non-existing user " + sshkey.User}
}
}
return nil
}
func customizeUserAndSSHKey(p *pipeline.Pipeline, c *blueprint.Customizations) error {
if len(c.User) == 0 {
if len(c.SSHKey) > 0 {
return &blueprint.CustomizationError{"SSH key customization defined but no user customizations are defined"}
}
return nil
}
// return error if ssh key customization without user defined in user customization if found
if e := assertAllUsersExistForSSHCustomizations(c); e != nil {
return e
}
users := make(map[string]pipeline.UsersStageOptionsUser)
for _, user := range c.User {
if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) {
cryptedPassword, err := crypt.CryptSHA512(*user.Password)
if err != nil {
return err
}
user.Password = &cryptedPassword
}
userData := pipeline.UsersStageOptionsUser{
Groups: user.Groups,
Description: user.Description,
Home: user.Home,
Shell: user.Shell,
Password: user.Password,
Key: user.Key,
}
if user.UID != nil {
uid := strconv.Itoa(*user.UID)
userData.UID = &uid
}
if user.GID != nil {
gid := strconv.Itoa(*user.GID)
userData.GID = &gid
}
// process sshkey customizations
if additionalKeys := findKeysForUser(c.SSHKey, user.Name); len(additionalKeys) > 0 {
joinedKeys := strings.Join(additionalKeys, "\n")
if userData.Key != nil {
*userData.Key += "\n" + joinedKeys
} else {
userData.Key = &joinedKeys
}
}
users[user.Name] = userData
}
p.AddStage(
pipeline.NewUsersStage(
&pipeline.UsersStageOptions{Users: users},
),
)
return nil
}
func customizeTimezone(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Timezone == nil || c.Timezone.Timezone == nil {
return
}
// TODO: lorax (anaconda) automatically installs chrony if timeservers are defined
// except for the case when chrony is explicitly removed from installed packages (using -chrony)
// this is currently not supported, no checks whether chrony is installed are not performed
p.AddStage(
pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{
Zone: *c.Timezone.Timezone,
}),
)
}
func customizeNTPServers(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Timezone == nil || len(c.Timezone.NTPServers) == 0 {
return
}
p.AddStage(
pipeline.NewChronyStage(&pipeline.ChronyStageOptions{
Timeservers: c.Timezone.NTPServers,
}),
)
}
func customizeLanguages(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Locale == nil || len(c.Locale.Languages) == 0 {
return
}
// TODO: you can specify more languages in customization
// The first one is the primary one, we can set in the locale stage, this should currently work
// Also, ALL the listed languages are installed using langpack-* packages
// This is currently not implemented!
// See anaconda src: pyanaconda/payload/dnfpayload.py:772
p.AddStage(
pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{
Language: c.Locale.Languages[0],
}),
)
}
func customizeKeyboard(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Locale == nil || c.Locale.Keyboard == nil {
return
}
p.AddStage(
pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{
Keymap: *c.Locale.Keyboard,
}),
)
}
func customizeFirewall(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Firewall == nil {
return
}
var enabledServices, disabledServices []string
if c.Firewall.Services != nil {
enabledServices = c.Firewall.Services.Enabled
disabledServices = c.Firewall.Services.Disabled
}
p.AddStage(
pipeline.NewFirewallStage(&pipeline.FirewallStageOptions{
Ports: c.Firewall.Ports,
EnabledServices: enabledServices,
DisabledServices: disabledServices,
}),
)
}
func customizeServices(p *pipeline.Pipeline, c *blueprint.Customizations) {
if c.Services == nil {
return
}
p.AddStage(
pipeline.NewSystemdStage(&pipeline.SystemdStageOptions{
EnabledServices: c.Services.Enabled,
DisabledServices: c.Services.Disabled,
}),
)
}
func findKeysForUser(sshKeyCustomizations []blueprint.SSHKeyCustomization, user string) (keys []string) {
for _, sshKey := range sshKeyCustomizations {
if sshKey.User == user {
keys = append(keys, sshKey.Key)
}
}
return
}
func userCustomizationsContainUsername(userCustomizations []blueprint.UserCustomization, name string) bool {
for _, usr := range userCustomizations {
if usr.Name == name {
return true
}
}
return false
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type diskOutput struct{}
func (t *diskOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *diskOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"@core",
"chrony",
@ -20,13 +23,13 @@ func (t *diskOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30FSTabStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30QemuAssembler(p, "raw", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type ext4Output struct{}
func (t *ext4Output) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *ext4Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"policycoreutils",
"selinux-policy-targeted",
@ -18,13 +21,13 @@ func (t *ext4Output) translate(b *Blueprint) (*pipeline.Pipeline, error) {
}
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30RawFSAssembler(p, t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,6 +1,7 @@
package blueprint
package fedora30
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
"github.com/google/uuid"
@ -89,7 +90,7 @@ func getF30Pipeline() *pipeline.Pipeline {
return p
}
func getCustomF30PackageSet(packages []string, excludedPackages []string, blueprint *Blueprint) *pipeline.Pipeline {
func getCustomF30PackageSet(packages []string, excludedPackages []string, blueprint *blueprint.Blueprint) *pipeline.Pipeline {
p := &pipeline.Pipeline{
BuildPipeline: getF30BuildPipeline(),
}
@ -122,7 +123,7 @@ func getCustomF30PackageSet(packages []string, excludedPackages []string, bluepr
return p
}
func addF30GRUB2Stage(p *pipeline.Pipeline, kernelCustomization *KernelCustomization) {
func addF30GRUB2Stage(p *pipeline.Pipeline, kernelCustomization *blueprint.KernelCustomization) {
id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
if err != nil {
panic("invalid UUID")

View file

@ -1,17 +1,20 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type liveIsoOutput struct{}
func (t *liveIsoOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *liveIsoOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
// TODO!
p := getF30Pipeline()
addF30SELinuxStage(p)
addF30QemuAssembler(p, "raw", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type openstackOutput struct{}
func (t *openstackOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *openstackOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"@Core",
"chrony",
@ -24,13 +27,13 @@ func (t *openstackOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30FSTabStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30QemuAssembler(p, "qcow2", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,59 @@
package fedora30
import (
"sort"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type Fedora30 struct {
outputs map[string]output
}
type output interface {
translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error)
getName() string
getMime() string
}
func init() {
distro.Register("fedora-30", &Fedora30{
outputs: map[string]output {
"ami": &amiOutput{},
"ext4-filesystem": &ext4Output{},
"live-iso": &liveIsoOutput{},
"partitioned-disk": &diskOutput{},
"qcow2": &qcow2Output{},
"openstack": &openstackOutput{},
"tar": &tarOutput{},
"vhd": &vhdOutput{},
"vmdk": &vmdkOutput{},
},
})
}
// ListOutputFormats returns a sorted list of the supported output formats
func (f *Fedora30) ListOutputFormats() []string {
formats := make([]string, 0, len(f.outputs))
for name := range f.outputs {
formats = append(formats, name)
}
sort.Strings(formats)
return formats
}
func (f *Fedora30) FilenameFromType(outputFormat string) (string, string, error) {
if output, exists := f.outputs[outputFormat]; exists {
return output.getName(), output.getMime(), nil
}
return "", "", &distro.InvalidOutputFormatError{outputFormat}
}
func (f *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
if output, exists := f.outputs[outputFormat]; exists {
return output.translate(b)
}
return nil, &distro.InvalidOutputFormatError{outputFormat}
}

View file

@ -0,0 +1,119 @@
package fedora30_test
import (
"reflect"
"testing"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
)
func TestListOutputFormats(t *testing.T) {
want := []string{
"ami",
"ext4-filesystem",
"live-iso",
"openstack",
"partitioned-disk",
"qcow2",
"tar",
"vhd",
"vmdk",
}
f30 := distro.New("fedora-30")
if got := f30.ListOutputFormats(); !reflect.DeepEqual(got, want) {
t.Errorf("ListOutputFormats() = %v, want %v", got, want)
}
}
func TestFilenameFromType(t *testing.T) {
type args struct {
outputFormat string
}
tests := []struct {
name string
args args
want string
want1 string
wantErr bool
}{
{
name: "ami",
args: args{"ami"},
want: "image.ami",
want1: "application/x-qemu-disk",
},
{
name: "ext4",
args: args{"ext4-filesystem"},
want: "filesystem.img",
want1: "application/octet-stream",
},
{
name: "live-iso",
args: args{"live-iso"},
want: "image.iso",
want1: "application/x-iso9660-image",
},
{
name: "openstack",
args: args{"openstack"},
want: "image.qcow2",
want1: "application/x-qemu-disk",
},
{
name: "partitioned-disk",
args: args{"partitioned-disk"},
want: "disk.img",
want1: "application/octet-stream",
},
{
name: "qcow2",
args: args{"qcow2"},
want: "image.qcow2",
want1: "application/x-qemu-disk",
},
{
name: "tar",
args: args{"tar"},
want: "root.tar.xz",
want1: "application/x-tar",
},
{
name: "vhd",
args: args{"vhd"},
want: "image.vhd",
want1: "application/x-vhd",
},
{
name: "vmdk",
args: args{"vmdk"},
want: "disk.vmdk",
want1: "application/x-vmdk",
},
{
name: "invalid-output-type",
args: args{"foobar"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f30 := distro.New("fedora-30")
got, got1, err := f30.FilenameFromType(tt.args.outputFormat)
if (err != nil) != tt.wantErr {
t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if got != tt.want {
t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
}
}
})
}
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type qcow2Output struct{}
func (t *qcow2Output) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *qcow2Output) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"kernel-core",
"@Fedora Cloud Server",
@ -25,13 +28,13 @@ func (t *qcow2Output) translate(b *Blueprint) (*pipeline.Pipeline, error) {
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30FSTabStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30QemuAssembler(p, "qcow2", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type tarOutput struct{}
func (t *tarOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *tarOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"policycoreutils",
"selinux-policy-targeted",
@ -18,13 +21,13 @@ func (t *tarOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
}
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30TarAssembler(p, t.getName(), "xz")
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type vhdOutput struct{}
func (t *vhdOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *vhdOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"@Core",
"chrony",
@ -23,13 +26,13 @@ func (t *vhdOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30FSTabStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30QemuAssembler(p, "qcow2", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -1,10 +1,13 @@
package blueprint
package fedora30
import "github.com/osbuild/osbuild-composer/internal/pipeline"
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/pipeline"
)
type vmdkOutput struct{}
func (t *vmdkOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
func (t *vmdkOutput) translate(b *blueprint.Blueprint) (*pipeline.Pipeline, error) {
packages := [...]string{
"@core",
"chrony",
@ -21,13 +24,13 @@ func (t *vmdkOutput) translate(b *Blueprint) (*pipeline.Pipeline, error) {
p := getCustomF30PackageSet(packages[:], excludedPackages[:], b)
addF30LocaleStage(p)
addF30FSTabStage(p)
addF30GRUB2Stage(p, b.getKernelCustomization())
addF30GRUB2Stage(p, b.GetKernelCustomization())
addF30FixBlsStage(p)
addF30SELinuxStage(p)
addF30QemuAssembler(p, "vmdk", t.getName())
if b.Customizations != nil {
err := b.Customizations.customizeAll(p)
err := customizeAll(p, b.Customizations)
if err != nil {
return nil, err
}

View file

@ -16,6 +16,9 @@ import (
"github.com/osbuild/osbuild-composer/internal/pipeline"
"github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
"github.com/google/uuid"
)
@ -393,7 +396,8 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
targets := []*target.Target{
target.NewLocalTarget(target.NewLocalTargetOptions("/var/lib/osbuild-composer/outputs/" + composeID.String())),
}
pipeline, err := bp.ToPipeline(composeType)
f30 := distro.New("fedora-30")
pipeline, err := f30.Pipeline(bp, composeType)
if err != nil {
return err
}
@ -471,7 +475,8 @@ func (s *Store) GetImage(composeID uuid.UUID) (*Image, error) {
if compose.QueueStatus != "FINISHED" {
return nil, &InvalidRequestError{"compose was not finished"}
}
name, mime, err := blueprint.FilenameFromType(compose.OutputType)
f30 := distro.New("fedora-30")
name, mime, err := f30.FilenameFromType(compose.OutputType)
if err != nil {
panic("invalid output type")
}

View file

@ -16,6 +16,9 @@ import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
)
type API struct {
@ -847,7 +850,8 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
Types []composeType `json:"types"`
}
for _, format := range blueprint.ListOutputFormats() {
f30 := distro.New("fedora-30")
for _, format := range f30.ListOutputFormats() {
reply.Types = append(reply.Types, composeType{format, true})
}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"filename": "image.ami",
"output-format": "ami",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "partitioned-disk",
"filename": "disk.img",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "ext4-filesystem",
"filename": "filesystem.img",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "openstack",
"filename": "image.qcow2",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "qcow2",
"filename": "image.qcow2",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "vhd",
"filename": "image.vhd",
"blueprint": {}

View file

@ -1,5 +1,6 @@
{
"compose": {
"distro": "fedora-30",
"output-format": "vmdk",
"filename": "disk.vmdk",
"blueprint": {}