diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go index 0add590f55..c8df14236f 100644 --- a/cmd/osbuild-composer/main.go +++ b/cmd/osbuild-composer/main.go @@ -91,14 +91,20 @@ func main() { } if l, exists := listeners["osbuild-composer.socket"]; exists { - if len(l) != 1 { - logrus.Fatal("The osbuild-composer.socket unit is misconfigured. It should contain only one socket.") + if len(l) != 2 { + logrus.Fatal("The osbuild-composer.socket unit is misconfigured. It should contain two sockets.") } err = composer.InitWeldr(repositoryConfigs, l[0], config.weldrDistrosImageTypeDenyList()) if err != nil { logrus.Fatalf("Error initializing weldr API: %v", err) } + + // Start cloudapi using the 2nd socket and no certs + err = composer.InitAPI(ServerCertFile, ServerKeyFile, false, false, false, l[1]) + if err != nil { + logrus.Fatalf("Error initializing Cloud API using local socket: %v", err) + } } if l, exists := listeners["osbuild-local-worker.socket"]; exists { diff --git a/distribution/osbuild-composer.socket b/distribution/osbuild-composer.socket index fe5dd229f8..90f7547b34 100644 --- a/distribution/osbuild-composer.socket +++ b/distribution/osbuild-composer.socket @@ -3,6 +3,7 @@ Description=OSBuild Composer Weldr API socket [Socket] ListenStream=/run/weldr/api.socket +ListenStream=/run/cloudapi/api.socket SocketGroup=weldr SocketMode=660 diff --git a/internal/cloudapi/v2/compose.go b/internal/cloudapi/v2/compose.go index f5e46ff2ed..842d377130 100644 --- a/internal/cloudapi/v2/compose.go +++ b/internal/cloudapi/v2/compose.go @@ -10,20 +10,491 @@ import ( "github.com/osbuild/osbuild-composer/internal/blueprint" ) -// GetBlueprintWithCustomizations returns a new Blueprint with all of the -// customizations set from the ComposeRequest -func (request *ComposeRequest) GetBlueprintWithCustomizations() (blueprint.Blueprint, error) { - var bp = blueprint.Blueprint{Name: "empty blueprint"} - err := bp.Initialize() +// Return the string representation of the partitioning mode +// default to auto-lvm (should never happen) +func (bcpm BlueprintCustomizationsPartitioningMode) String() string { + switch bcpm { + case BlueprintCustomizationsPartitioningModeAutoLvm: + return "auto-lvm" + case BlueprintCustomizationsPartitioningModeLvm: + return "lvm" + case BlueprintCustomizationsPartitioningModeRaw: + return "raw" + default: + return "auto-lvm" + } +} + +// GetCustomizationsFromBlueprintRequest populates a blueprint customization struct +// with the data from the blueprint section of a ComposeRequest, which is similar but +// slightly different from the Cloudapi's Customizations section +// This starts with a new empty blueprint.Customization object +// If there are no customizations, it returns nil +func (request *ComposeRequest) GetCustomizationsFromBlueprintRequest() (*blueprint.Customizations, error) { + if request.Blueprint.Customizations == nil { + return nil, nil + } + + c := &blueprint.Customizations{} + rbpc := request.Blueprint.Customizations + + if rbpc.Hostname != nil { + c.Hostname = rbpc.Hostname + } + + if rbpc.Kernel != nil { + kernel := &blueprint.KernelCustomization{} + if rbpc.Kernel.Name != nil { + kernel.Name = *rbpc.Kernel.Name + } + if rbpc.Kernel.Append != nil { + kernel.Append = *rbpc.Kernel.Append + } + + c.Kernel = kernel + } + + if rbpc.Sshkey != nil { + keys := []blueprint.SSHKeyCustomization{} + for _, key := range *rbpc.Sshkey { + keys = append(keys, blueprint.SSHKeyCustomization{ + User: key.User, + Key: key.Key, + }) + } + c.SSHKey = keys + } + + if rbpc.User != nil { + var userCustomizations []blueprint.UserCustomization + for _, user := range *rbpc.User { + uc := blueprint.UserCustomization{ + Name: user.Name, + Description: user.Description, + Password: user.Password, + Key: user.Key, + Home: user.Home, + Shell: user.Shell, + UID: user.Uid, + GID: user.Gid, + } + if user.Groups != nil { + uc.Groups = append(uc.Groups, *user.Groups...) + } + userCustomizations = append(userCustomizations, uc) + } + c.User = userCustomizations + } + + if rbpc.Group != nil { + var groupCustomizations []blueprint.GroupCustomization + for _, group := range *rbpc.Group { + gc := blueprint.GroupCustomization{ + Name: group.Name, + GID: group.Gid, + } + groupCustomizations = append(groupCustomizations, gc) + } + c.Group = groupCustomizations + + } + + if rbpc.Timezone != nil { + tz := &blueprint.TimezoneCustomization{ + Timezone: rbpc.Timezone.Timezone, + } + + if rbpc.Timezone.Ntpservers != nil { + tz.NTPServers = append(tz.NTPServers, *rbpc.Timezone.Ntpservers...) + } + + c.Timezone = tz + } + + if rbpc.Locale != nil { + locale := &blueprint.LocaleCustomization{ + Keyboard: rbpc.Locale.Keyboard, + } + + if rbpc.Locale.Languages != nil { + locale.Languages = append(locale.Languages, *rbpc.Locale.Languages...) + } + + c.Locale = locale + } + + if rbpc.Firewall != nil { + firewall := &blueprint.FirewallCustomization{} + if rbpc.Firewall.Ports != nil { + firewall.Ports = append(firewall.Ports, *rbpc.Firewall.Ports...) + } + if rbpc.Firewall.Services != nil { + enabled := []string{} + if rbpc.Firewall.Services.Enabled != nil { + enabled = append(enabled, *rbpc.Firewall.Services.Enabled...) + } + disabled := []string{} + if rbpc.Firewall.Services.Disabled != nil { + disabled = append(disabled, *rbpc.Firewall.Services.Disabled...) + } + firewall.Services = &blueprint.FirewallServicesCustomization{ + Enabled: enabled, + Disabled: disabled, + } + } + if rbpc.Firewall.Zones != nil { + var zones []blueprint.FirewallZoneCustomization + for _, zone := range *rbpc.Firewall.Zones { + zc := blueprint.FirewallZoneCustomization{} + if zone.Name != nil { + zc.Name = zone.Name + } + if zone.Sources != nil { + zc.Sources = append(zc.Sources, *zone.Sources...) + } + zones = append(zones, zc) + } + firewall.Zones = zones + } + + c.Firewall = firewall + } + + if rbpc.Services != nil { + servicesCustomization := &blueprint.ServicesCustomization{} + if rbpc.Services.Enabled != nil { + servicesCustomization.Enabled = make([]string, len(*rbpc.Services.Enabled)) + copy(servicesCustomization.Enabled, *rbpc.Services.Enabled) + } + if rbpc.Services.Disabled != nil { + servicesCustomization.Disabled = make([]string, len(*rbpc.Services.Disabled)) + copy(servicesCustomization.Disabled, *rbpc.Services.Disabled) + } + c.Services = servicesCustomization + } + + if rbpc.Filesystem != nil { + var fsCustomizations []blueprint.FilesystemCustomization + for _, f := range *rbpc.Filesystem { + fsCustomizations = append(fsCustomizations, + blueprint.FilesystemCustomization{ + Mountpoint: f.Mountpoint, + MinSize: f.Minsize, + }, + ) + } + c.Filesystem = fsCustomizations + } + + if rbpc.InstallationDevice != nil { + c.InstallationDevice = *rbpc.InstallationDevice + } + + if rbpc.PartitioningMode != nil { + c.PartitioningMode = string(*rbpc.PartitioningMode) + } + + if rbpc.Fdo != nil { + fdo := &blueprint.FDOCustomization{} + if rbpc.Fdo.DiunPubKeyHash != nil { + fdo.DiunPubKeyHash = *rbpc.Fdo.DiunPubKeyHash + } + if rbpc.Fdo.DiunPubKeyInsecure != nil { + fdo.DiunPubKeyInsecure = *rbpc.Fdo.DiunPubKeyInsecure + } + if rbpc.Fdo.DiunPubKeyRootCerts != nil { + fdo.DiunPubKeyRootCerts = *rbpc.Fdo.DiunPubKeyRootCerts + } + if rbpc.Fdo.DiMfgStringTypeMacIface != nil { + fdo.DiMfgStringTypeMacIface = *rbpc.Fdo.DiMfgStringTypeMacIface + } + if rbpc.Fdo.ManufacturingServerUrl != nil { + fdo.ManufacturingServerURL = *rbpc.Fdo.ManufacturingServerUrl + } + + c.FDO = fdo + } + + if rbpc.Openscap != nil { + oscap := &blueprint.OpenSCAPCustomization{ + ProfileID: rbpc.Openscap.ProfileId, + } + if rbpc.Openscap.Datastream != nil { + oscap.DataStream = *rbpc.Openscap.Datastream + } + if tailoring := rbpc.Openscap.Tailoring; tailoring != nil { + tc := blueprint.OpenSCAPTailoringCustomizations{} + if tailoring.Selected != nil && len(*tailoring.Selected) > 0 { + tc.Selected = append(tc.Selected, *tailoring.Selected...) + } + if tailoring.Unselected != nil && len(*tailoring.Unselected) > 0 { + tc.Unselected = append(tc.Unselected, *tailoring.Unselected...) + } + oscap.Tailoring = &tc + } + c.OpenSCAP = oscap + } + + if rbpc.Ignition != nil { + ignition := &blueprint.IgnitionCustomization{} + if rbpc.Ignition.Embedded != nil { + ignition.Embedded = &blueprint.EmbeddedIgnitionCustomization{ + Config: rbpc.Ignition.Embedded.Config, + } + } + if rbpc.Ignition.Firstboot != nil { + ignition.FirstBoot = &blueprint.FirstBootIgnitionCustomization{ + ProvisioningURL: rbpc.Ignition.Firstboot.Url, + } + } + c.Ignition = ignition + } + + if rbpc.Directories != nil { + var dirCustomizations []blueprint.DirectoryCustomization + for _, d := range *rbpc.Directories { + dirCustomization := blueprint.DirectoryCustomization{ + Path: d.Path, + } + if d.Mode != nil { + dirCustomization.Mode = *d.Mode + } + if d.User != nil { + dirCustomization.User = *d.User + if uid, ok := dirCustomization.User.(float64); ok { + // check if uid can be converted to int64 + if uid != float64(int64(uid)) { + return nil, fmt.Errorf("invalid user %f: must be an integer", uid) + } + dirCustomization.User = int64(uid) + } + } + if d.Group != nil { + dirCustomization.Group = *d.Group + if gid, ok := dirCustomization.Group.(float64); ok { + // check if gid can be converted to int64 + if gid != float64(int64(gid)) { + return nil, fmt.Errorf("invalid group %f: must be an integer", gid) + } + dirCustomization.Group = int64(gid) + } + } + if d.EnsureParents != nil { + dirCustomization.EnsureParents = *d.EnsureParents + } + dirCustomizations = append(dirCustomizations, dirCustomization) + } + + // Validate the directory customizations, because the Cloud API does not use the custom unmarshaller + _, err := blueprint.DirectoryCustomizationsToFsNodeDirectories(dirCustomizations) + if err != nil { + return nil, HTTPErrorWithInternal(ErrorInvalidCustomization, err) + } + + c.Directories = dirCustomizations + } + + if rbpc.Files != nil { + var fileCustomizations []blueprint.FileCustomization + for _, f := range *rbpc.Files { + fileCustomization := blueprint.FileCustomization{ + Path: f.Path, + } + if f.Data != nil { + fileCustomization.Data = *f.Data + } + if f.Mode != nil { + fileCustomization.Mode = *f.Mode + } + if f.User != nil { + fileCustomization.User = *f.User + if uid, ok := fileCustomization.User.(float64); ok { + // check if uid can be converted to int64 + if uid != float64(int64(uid)) { + return nil, fmt.Errorf("invalid user %f: must be an integer", uid) + } + fileCustomization.User = int64(uid) + } + } + if f.Group != nil { + fileCustomization.Group = *f.Group + if gid, ok := fileCustomization.Group.(float64); ok { + // check if gid can be converted to int64 + if gid != float64(int64(gid)) { + return nil, fmt.Errorf("invalid group %f: must be an integer", gid) + } + fileCustomization.Group = int64(gid) + } + } + fileCustomizations = append(fileCustomizations, fileCustomization) + } + + // Validate the file customizations, because the Cloud API does not use the custom unmarshaller + _, err := blueprint.FileCustomizationsToFsNodeFiles(fileCustomizations) + if err != nil { + return nil, HTTPErrorWithInternal(ErrorInvalidCustomization, err) + } + + c.Files = fileCustomizations + } + + if rbpc.Repositories != nil { + repoCustomizations := []blueprint.RepositoryCustomization{} + for _, repo := range *rbpc.Repositories { + repoCustomization := blueprint.RepositoryCustomization{ + Id: repo.Id, + } + + if repo.Name != nil { + repoCustomization.Name = *repo.Name + } + + if repo.Filename != nil { + repoCustomization.Filename = *repo.Filename + } + + if repo.Baseurls != nil && len(*repo.Baseurls) > 0 { + repoCustomization.BaseURLs = append(repoCustomization.BaseURLs, *repo.Baseurls...) + } + + if repo.Gpgkeys != nil && len(*repo.Gpgkeys) > 0 { + repoCustomization.GPGKeys = append(repoCustomization.GPGKeys, *repo.Gpgkeys...) + } + + if repo.Gpgcheck != nil { + repoCustomization.GPGCheck = repo.Gpgcheck + } + + if repo.RepoGpgcheck != nil { + repoCustomization.RepoGPGCheck = repo.RepoGpgcheck + } + + 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 + } + + if repo.Priority != nil { + repoCustomization.Priority = repo.Priority + } + + if repo.ModuleHotfixes != nil { + repoCustomization.ModuleHotfixes = repo.ModuleHotfixes + } + + repoCustomizations = append(repoCustomizations, repoCustomization) + } + c.Repositories = repoCustomizations + } + + if rbpc.Fips != nil { + c.FIPS = rbpc.Fips + } + + return c, nil +} + +// GetBlueprintFromCompose returns a base blueprint +// It is either constructed from the Blueprint passed in with the request, or it +// is an empty blueprint +func (request *ComposeRequest) GetBlueprintFromCompose() (blueprint.Blueprint, error) { + // nil or blank blueprint returns a valid empty blueprint + if request.Blueprint == nil || reflect.DeepEqual(*request.Blueprint, Blueprint{}) { + bp := blueprint.Blueprint{Name: "empty blueprint"} + err := bp.Initialize() + return bp, err + } + + var bp blueprint.Blueprint + rbp := request.Blueprint + + // Copy all the parts from the OpenAPI Blueprint into a blueprint.Blueprint + // NOTE: Openapi fields may be nil, test for that first. + bp.Name = rbp.Name + if rbp.Description != nil { + bp.Description = *rbp.Description + } + if rbp.Version != nil { + bp.Version = *rbp.Version + } + if rbp.Distro != nil { + bp.Distro = *rbp.Distro + } + + if rbp.Packages != nil { + for _, pkg := range *rbp.Packages { + newPkg := blueprint.Package{Name: pkg.Name} + if pkg.Version != nil { + newPkg.Version = *pkg.Version + } + bp.Packages = append(bp.Packages, newPkg) + } + } + + if rbp.Modules != nil { + for _, pkg := range *rbp.Modules { + newPkg := blueprint.Package{Name: pkg.Name} + if pkg.Version != nil { + newPkg.Version = *pkg.Version + } + bp.Modules = append(bp.Modules, newPkg) + } + } + + if rbp.Groups != nil { + for _, group := range *rbp.Groups { + bp.Groups = append(bp.Groups, blueprint.Group{ + Name: group.Name, + }) + } + } + + if rbp.Containers != nil { + for _, c := range *rbp.Containers { + newC := blueprint.Container{Source: c.Source, TLSVerify: c.TlsVerify} + if c.Name != nil { + newC.Name = *c.Name + } + bp.Containers = append(bp.Containers, newC) + } + } + + customizations, err := request.GetCustomizationsFromBlueprintRequest() + if err != nil { + return bp, err + } + bp.Customizations = customizations + + err = bp.Initialize() if err != nil { return bp, HTTPErrorWithInternal(ErrorFailedToInitializeBlueprint, err) } + return bp, nil +} + +// GetBlueprintFromCustomizations returns a new Blueprint with all of the +// customizations set from the ComposeRequest.Customizations +func (request *ComposeRequest) GetBlueprintFromCustomizations() (blueprint.Blueprint, error) { + bp := blueprint.Blueprint{Name: "empty blueprint"} + err := bp.Initialize() + if err != nil { + return bp, HTTPErrorWithInternal(ErrorFailedToInitializeBlueprint, err) + } if request.Customizations == nil { return bp, nil } - - // Assume there is going to be one or more customization bp.Customizations = &blueprint.Customizations{} // Set the blueprint customisation to take care of the user @@ -394,6 +865,18 @@ func (request *ComposeRequest) GetBlueprintWithCustomizations() (blueprint.Bluep return bp, nil } +// GetBlueprint returns a blueprint +// If the compose request includes a blueprint, return it, otherwise if it has +// customizations create a blueprint with those customizations. If it has neither +// return an empty blueprint. +func (request *ComposeRequest) GetBlueprint() (blueprint.Blueprint, error) { + if request.Blueprint != nil { + return request.GetBlueprintFromCompose() + } + + return request.GetBlueprintFromCustomizations() +} + // GetPayloadRepositories returns the custom repos // If there are none it returns a nil slice func (request *ComposeRequest) GetPayloadRepositories() (repos []Repository) { diff --git a/internal/cloudapi/v2/compose_test.go b/internal/cloudapi/v2/compose_test.go index 8c3589eb64..7d90177581 100644 --- a/internal/cloudapi/v2/compose_test.go +++ b/internal/cloudapi/v2/compose_test.go @@ -12,10 +12,129 @@ import ( "github.com/stretchr/testify/require" ) -func TestGetBlueprintWithCustomizations(t *testing.T) { +// GetTestBlueprint returns a populated blueprint +// This is used in testing the Customizations compose request +// and the Blueprint compose request. +// They both result in the same final blueprint used to create the compose +func GetTestBlueprint() blueprint.Blueprint { + // Test with customizations + expected := blueprint.Blueprint{Name: "empty blueprint"} + err := expected.Initialize() + // An empty blueprint should never fail to initialize + if err != nil { + panic(err) + } + + // Construct the expected blueprint result + // Packages + expected.Packages = []blueprint.Package{ + {Name: "bash"}, + {Name: "tmux"}, + } + + // Containers + expected.Containers = []blueprint.Container{ + blueprint.Container{ + Name: "container-name", + Source: "http://some.path.to/a/container/source", + }, + } + + // Customizations + expected.Customizations = &blueprint.Customizations{ + User: []blueprint.UserCustomization{ + blueprint.UserCustomization{ + Name: "admin", + Key: common.ToPtr("dummy ssh-key"), + Groups: []string{"users", "wheel"}, + }, + }, + Directories: []blueprint.DirectoryCustomization{ + blueprint.DirectoryCustomization{ + Path: "/opt/extra", + User: "root", + Group: "root", + Mode: "0755", + EnsureParents: true, + }, + }, + Files: []blueprint.FileCustomization{ + blueprint.FileCustomization{ + Path: "/etc/mad.conf", + User: "root", + Group: "root", + Mode: "0644", + Data: "Alfred E. Neuman was here.\n", + }, + }, + Filesystem: []blueprint.FilesystemCustomization{ + blueprint.FilesystemCustomization{ + Mountpoint: "/var/lib/wopr/", + MinSize: 1099511627776, + }, + }, + Services: &blueprint.ServicesCustomization{ + Enabled: []string{"sshd"}, + Disabled: []string{"cleanup"}, + }, + OpenSCAP: &blueprint.OpenSCAPCustomization{ + ProfileID: "B 263-59", + }, + Repositories: []blueprint.RepositoryCustomization{ + blueprint.RepositoryCustomization{ + Id: "custom repo", + Metalink: "http://example.org/metalink", + Enabled: common.ToPtr(true), + GPGCheck: common.ToPtr(true), + ModuleHotfixes: common.ToPtr(true), + }, + }, + Firewall: &blueprint.FirewallCustomization{ + Ports: []string{ + "22/tcp", + }, + }, + Hostname: common.ToPtr("hostname"), + InstallationDevice: "/dev/sda", + Kernel: &blueprint.KernelCustomization{ + Append: "nosmt=force", + Name: "kernel-debug", + }, + Locale: &blueprint.LocaleCustomization{ + Keyboard: common.ToPtr("us"), + Languages: []string{ + "en_US.UTF-8", + }, + }, + FDO: &blueprint.FDOCustomization{ + DiunPubKeyHash: "pubkeyhash", + DiunPubKeyInsecure: "pubkeyinsecure", + DiunPubKeyRootCerts: "pubkeyrootcerts", + ManufacturingServerURL: "serverurl", + DiMfgStringTypeMacIface: "iface", + }, + Ignition: &blueprint.IgnitionCustomization{ + FirstBoot: &blueprint.FirstBootIgnitionCustomization{ + ProvisioningURL: "provisioning-url.local", + }, + }, + Timezone: &blueprint.TimezoneCustomization{ + Timezone: common.ToPtr("US/Eastern"), + NTPServers: []string{ + "0.north-america.pool.ntp.org", + "1.north-america.pool.ntp.org", + }, + }, + FIPS: common.ToPtr(true), + } + + return expected +} + +func TestGetBlueprintFromCustomizations(t *testing.T) { // Empty request should return empty blueprint cr := ComposeRequest{} - bp, err := cr.GetBlueprintWithCustomizations() + bp, err := cr.GetBlueprintFromCustomizations() require.Nil(t, err) assert.Equal(t, "empty blueprint", bp.Name) assert.Equal(t, "0.0.0", bp.Version) @@ -25,17 +144,12 @@ func TestGetBlueprintWithCustomizations(t *testing.T) { cr = ComposeRequest{ Customizations: &Customizations{}, } - bp, err = cr.GetBlueprintWithCustomizations() + bp, err = cr.GetBlueprintFromCustomizations() require.Nil(t, err) assert.Equal(t, "empty blueprint", bp.Name) assert.Equal(t, "0.0.0", bp.Version) assert.Nil(t, bp.Customizations) - // Test with customizations - expected := blueprint.Blueprint{Name: "empty blueprint"} - err = expected.Initialize() - require.Nil(t, err) - // interface{} is a terrible idea. Work around it... var rootStr interface{} = "root" @@ -114,8 +228,8 @@ func TestGetBlueprintWithCustomizations(t *testing.T) { DiunPubKeyHash: common.ToPtr("pubkeyhash"), DiunPubKeyInsecure: common.ToPtr("pubkeyinsecure"), DiunPubKeyRootCerts: common.ToPtr("pubkeyrootcerts"), - ManufacturingServerUrl: common.ToPtr("serverurl"), DiMfgStringTypeMacIface: common.ToPtr("iface"), + ManufacturingServerUrl: common.ToPtr("serverurl"), }, Ignition: &Ignition{ Firstboot: &IgnitionFirstboot{ @@ -132,111 +246,142 @@ func TestGetBlueprintWithCustomizations(t *testing.T) { Fips: &FIPS{Enabled: common.ToPtr(true)}, }} - // Construct the expected blueprint result - // Packages - expected.Packages = []blueprint.Package{ - {Name: "bash"}, - {Name: "tmux"}, - } + bp, err = cr.GetBlueprintFromCustomizations() + require.Nil(t, err) + assert.Equal(t, GetTestBlueprint(), bp) +} - // Containers - expected.Containers = []blueprint.Container{ - blueprint.Container{ - Name: "container-name", - Source: "http://some.path.to/a/container/source", - }, +func TestGetBlueprintFromCompose(t *testing.T) { + // Empty request should return empty blueprint + cr := ComposeRequest{} + bp, err := cr.GetBlueprintFromCompose() + require.Nil(t, err) + assert.Equal(t, "empty blueprint", bp.Name) + assert.Equal(t, "0.0.0", bp.Version) + assert.Nil(t, bp.Customizations) + + // Empty request should return empty blueprint + cr = ComposeRequest{ + Blueprint: &Blueprint{}, } + bp, err = cr.GetBlueprintFromCompose() + require.Nil(t, err) + assert.Equal(t, "empty blueprint", bp.Name) + assert.Equal(t, "0.0.0", bp.Version) + assert.Nil(t, bp.Customizations) - // Customizations - expected.Customizations = &blueprint.Customizations{ - User: []blueprint.UserCustomization{ - blueprint.UserCustomization{ - Name: "admin", - Key: common.ToPtr("dummy ssh-key"), - Groups: []string{"users", "wheel"}, + // interface{} is a terrible idea. Work around it... + var rootStr interface{} = "root" + + // Construct the compose request with a blueprint + cr = ComposeRequest{Blueprint: &Blueprint{ + Name: "empty blueprint", + Version: common.ToPtr("0.0.0"), + Packages: &[]Package{{Name: "bash"}, {Name: "tmux"}}, + Containers: &[]Container{ + Container{ + Name: common.ToPtr("container-name"), + Source: "http://some.path.to/a/container/source", }, }, - Directories: []blueprint.DirectoryCustomization{ - blueprint.DirectoryCustomization{ - Path: "/opt/extra", - User: "root", - Group: "root", - Mode: "0755", - EnsureParents: true, + Customizations: &BlueprintCustomizations{ + User: &[]BlueprintUser{ + { + Name: "admin", + Key: common.ToPtr("dummy ssh-key"), + Groups: &[]string{"users", "wheel"}, + }}, + Directories: &[]Directory{ + Directory{ + Path: "/opt/extra", + EnsureParents: common.ToPtr(true), + User: &rootStr, + Group: &rootStr, + Mode: common.ToPtr("0755"), + }, }, - }, - Files: []blueprint.FileCustomization{ - blueprint.FileCustomization{ - Path: "/etc/mad.conf", - User: "root", - Group: "root", - Mode: "0644", - Data: "Alfred E. Neuman was here.\n", + Files: &[]BlueprintFile{ + { + Path: "/etc/mad.conf", + Data: common.ToPtr("Alfred E. Neuman was here.\n"), + User: &rootStr, + Group: &rootStr, + Mode: common.ToPtr("0644"), + }, }, - }, - Filesystem: []blueprint.FilesystemCustomization{ - blueprint.FilesystemCustomization{ - Mountpoint: "/var/lib/wopr/", - MinSize: 1099511627776, + Filesystem: &[]BlueprintFilesystem{ + { + Mountpoint: "/var/lib/wopr/", + Minsize: 1099511627776, + }, }, - }, - Services: &blueprint.ServicesCustomization{ - Enabled: []string{"sshd"}, - Disabled: []string{"cleanup"}, - }, - OpenSCAP: &blueprint.OpenSCAPCustomization{ - ProfileID: "B 263-59", - }, - Repositories: []blueprint.RepositoryCustomization{ - blueprint.RepositoryCustomization{ - Id: "custom repo", - Metalink: "http://example.org/metalink", - Enabled: common.ToPtr(true), - GPGCheck: common.ToPtr(true), - ModuleHotfixes: common.ToPtr(true), + Services: &Services{ + Disabled: &[]string{"cleanup"}, + Enabled: &[]string{"sshd"}, }, - }, - Firewall: &blueprint.FirewallCustomization{ - Ports: []string{ - "22/tcp", + Openscap: &BlueprintOpenSCAP{ProfileId: "B 263-59"}, + Repositories: &[]BlueprintRepository{ + { + Id: "custom repo", + Metalink: common.ToPtr("http://example.org/metalink"), + Gpgcheck: common.ToPtr(true), + Enabled: common.ToPtr(true), + ModuleHotfixes: common.ToPtr(true), + }, }, - }, - Hostname: common.ToPtr("hostname"), - InstallationDevice: "/dev/sda", - Kernel: &blueprint.KernelCustomization{ - Append: "nosmt=force", - Name: "kernel-debug", - }, - Locale: &blueprint.LocaleCustomization{ - Keyboard: common.ToPtr("us"), - Languages: []string{ - "en_US.UTF-8", + Firewall: &BlueprintFirewall{ + Ports: common.ToPtr([]string{ + "22/tcp", + }), }, - }, - FDO: &blueprint.FDOCustomization{ - DiunPubKeyHash: "pubkeyhash", - DiunPubKeyInsecure: "pubkeyinsecure", - DiunPubKeyRootCerts: "pubkeyrootcerts", - ManufacturingServerURL: "serverurl", - DiMfgStringTypeMacIface: "iface", - }, - Ignition: &blueprint.IgnitionCustomization{ - FirstBoot: &blueprint.FirstBootIgnitionCustomization{ - ProvisioningURL: "provisioning-url.local", + Hostname: common.ToPtr("hostname"), + InstallationDevice: common.ToPtr("/dev/sda"), + Kernel: &Kernel{ + Append: common.ToPtr("nosmt=force"), + Name: common.ToPtr("kernel-debug"), }, - }, - Timezone: &blueprint.TimezoneCustomization{ - Timezone: common.ToPtr("US/Eastern"), - NTPServers: []string{ - "0.north-america.pool.ntp.org", - "1.north-america.pool.ntp.org", + Locale: &Locale{ + Keyboard: common.ToPtr("us"), + Languages: common.ToPtr([]string{ + "en_US.UTF-8", + }), + }, + Fdo: &FDO{ + DiunPubKeyHash: common.ToPtr("pubkeyhash"), + DiunPubKeyInsecure: common.ToPtr("pubkeyinsecure"), + DiunPubKeyRootCerts: common.ToPtr("pubkeyrootcerts"), + DiMfgStringTypeMacIface: common.ToPtr("iface"), + ManufacturingServerUrl: common.ToPtr("serverurl"), }, + Ignition: &Ignition{ + Firstboot: &IgnitionFirstboot{ + Url: "provisioning-url.local", + }, + }, + Timezone: &Timezone{ + Timezone: common.ToPtr("US/Eastern"), + Ntpservers: common.ToPtr([]string{ + "0.north-america.pool.ntp.org", + "1.north-america.pool.ntp.org", + }), + }, + Fips: common.ToPtr(true), }, - FIPS: common.ToPtr(true), - } - bp, err = cr.GetBlueprintWithCustomizations() + }} + + bp, err = cr.GetBlueprintFromCompose() + require.Nil(t, err) + assert.Equal(t, GetTestBlueprint(), bp) +} + +func TestGetBlueprint(t *testing.T) { + cr := ComposeRequest{} + bp, err := cr.GetBlueprint() + require.Nil(t, err) require.Nil(t, err) - assert.Equal(t, bp, expected) + assert.Equal(t, "empty blueprint", bp.Name) + assert.Equal(t, "0.0.0", bp.Version) + assert.Nil(t, bp.Customizations) } func TestGetPayloadRepositories(t *testing.T) { diff --git a/internal/cloudapi/v2/errors.go b/internal/cloudapi/v2/errors.go index f2a058c9a1..565078e0a5 100644 --- a/internal/cloudapi/v2/errors.go +++ b/internal/cloudapi/v2/errors.go @@ -49,6 +49,7 @@ const ( ErrorLocalSaveNotEnabled ServiceErrorCode = 36 ErrorInvalidPartitioningMode ServiceErrorCode = 37 ErrorInvalidUploadTarget ServiceErrorCode = 38 + ErrorBlueprintOrCustomNotBoth ServiceErrorCode = 39 // Internal errors, these are bugs ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000 @@ -129,6 +130,7 @@ func getServiceErrors() serviceErrors { serviceError{ErrorLocalSaveNotEnabled, http.StatusBadRequest, "local_save is not enabled"}, serviceError{ErrorInvalidPartitioningMode, http.StatusBadRequest, "Requested partitioning mode is invalid"}, serviceError{ErrorInvalidUploadTarget, http.StatusBadRequest, "Invalid upload target for image type"}, + serviceError{ErrorBlueprintOrCustomNotBoth, http.StatusBadRequest, "Invalid request, include blueprint or customizations, not both"}, serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"}, serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"}, diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go index 4c8e529aa9..7a551e6831 100644 --- a/internal/cloudapi/v2/handler.go +++ b/internal/cloudapi/v2/handler.go @@ -160,8 +160,14 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error { return HTTPError(ErrorUnsupportedDistribution) } - // Create a blueprint from the customizations included in the request - bp, err := request.GetBlueprintWithCustomizations() + // OpenAPI enforces blueprint or customization, not both + // but check anyway + if request.Customizations != nil && request.Blueprint != nil { + return HTTPError(ErrorBlueprintOrCustomNotBoth) + } + + // Create a blueprint from the request + bp, err := request.GetBlueprint() if err != nil { return err } diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go index 33272dace0..e10857eb84 100644 --- a/internal/cloudapi/v2/openapi.v2.gen.go +++ b/internal/cloudapi/v2/openapi.v2.gen.go @@ -22,6 +22,15 @@ const ( BearerScopes = "Bearer.Scopes" ) +// Defines values for BlueprintCustomizationsPartitioningMode. +const ( + BlueprintCustomizationsPartitioningModeAutoLvm BlueprintCustomizationsPartitioningMode = "auto-lvm" + + BlueprintCustomizationsPartitioningModeLvm BlueprintCustomizationsPartitioningMode = "lvm" + + BlueprintCustomizationsPartitioningModeRaw BlueprintCustomizationsPartitioningMode = "raw" +) + // Defines values for ComposeStatusValue. const ( ComposeStatusValueFailure ComposeStatusValue = "failure" @@ -200,6 +209,185 @@ type AzureUploadStatus struct { ImageName string `json:"image_name"` } +// Blueprint defines model for Blueprint. +type Blueprint struct { + // Container images to embed into the final artfact + Containers *[]Container `json:"containers,omitempty"` + Customizations *BlueprintCustomizations `json:"customizations,omitempty"` + Description *string `json:"description,omitempty"` + + // The distribution to use for the compose. If left empty the host + // distro will be used. + Distro *string `json:"distro,omitempty"` + + // Package groups to be installed + Groups *[]PackageGroup `json:"groups,omitempty"` + + // An alias for packages, retained for backwards compatability + Modules *[]Package `json:"modules,omitempty"` + Name string `json:"name"` + + // Packages to be installed + Packages *[]Package `json:"packages,omitempty"` + + // A semver version number + Version *string `json:"version,omitempty"` +} + +// BlueprintCustomizations defines model for BlueprintCustomizations. +type BlueprintCustomizations struct { + // Directories to create in the final artifact + Directories *[]Directory `json:"directories,omitempty"` + + // FIDO device onboard configuration + Fdo *FDO `json:"fdo,omitempty"` + + // Files to create in the final artifact + Files *[]BlueprintFile `json:"files,omitempty"` + + // List of filesystem mountpoints to create + Filesystem *[]BlueprintFilesystem `json:"filesystem,omitempty"` + + // Enable FIPS mode + Fips *bool `json:"fips,omitempty"` + + // Firewalld configuration + Firewall *BlueprintFirewall `json:"firewall,omitempty"` + + // List of groups to create + Group *[]Group `json:"group,omitempty"` + + // Configures the hostname + Hostname *string `json:"hostname,omitempty"` + + // Ignition configuration + Ignition *Ignition `json:"ignition,omitempty"` + + // Name of the installation device, currently only useful for the edge-simplified-installer type + InstallationDevice *string `json:"installation_device,omitempty"` + Kernel *Kernel `json:"kernel,omitempty"` + + // Locale configuration + Locale *Locale `json:"locale,omitempty"` + Openscap *BlueprintOpenSCAP `json:"openscap,omitempty"` + + // Select how the disk image will be partitioned. 'auto-lvm' will use raw unless + // there are one or more mountpoints in which case it will use LVM. 'lvm' always + // uses LVM, even when there are no extra mountpoints. 'raw' uses raw partitions + // even when there are one or more mountpoints. + PartitioningMode *BlueprintCustomizationsPartitioningMode `json:"partitioning_mode,omitempty"` + + // Repositories to write to /etc/yum.repos.d/ in the final image. Note + // that these are not used at build time. + Repositories *[]BlueprintRepository `json:"repositories,omitempty"` + Services *Services `json:"services,omitempty"` + + // List of ssh keys + Sshkey *[]SSHKey `json:"sshkey,omitempty"` + + // Timezone configuration + Timezone *Timezone `json:"timezone,omitempty"` + + // List of users to create + User *[]BlueprintUser `json:"user,omitempty"` +} + +// Select how the disk image will be partitioned. 'auto-lvm' will use raw unless +// there are one or more mountpoints in which case it will use LVM. 'lvm' always +// uses LVM, even when there are no extra mountpoints. 'raw' uses raw partitions +// even when there are one or more mountpoints. +type BlueprintCustomizationsPartitioningMode string + +// A custom file to create in the final artifact. +type BlueprintFile struct { + // Contents of the file as plain text + Data *string `json:"data,omitempty"` + + // Group of the file as a gid or a group name + Group *interface{} `json:"group,omitempty"` + + // Permissions string for the file in octal format + Mode *string `json:"mode,omitempty"` + + // Path to the file + Path string `json:"path"` + + // Owner of the file as a uid or a user name + User *interface{} `json:"user,omitempty"` +} + +// BlueprintFilesystem defines model for BlueprintFilesystem. +type BlueprintFilesystem struct { + // size of the filesystem in bytes + Minsize uint64 `json:"minsize"` + Mountpoint string `json:"mountpoint"` +} + +// Firewalld configuration +type BlueprintFirewall struct { + // List of ports (or port ranges) and protocols to open + Ports *[]string `json:"ports,omitempty"` + + // Firewalld services to enable or disable + Services *FirewallServices `json:"services,omitempty"` + Zones *[]FirewallZones `json:"zones,omitempty"` +} + +// BlueprintOpenSCAP defines model for BlueprintOpenSCAP. +type BlueprintOpenSCAP struct { + Datastream *string `json:"datastream,omitempty"` + ProfileId string `json:"profile_id"` + Tailoring *OpenSCAPTailoring `json:"tailoring,omitempty"` +} + +// BlueprintRepository defines model for BlueprintRepository. +type BlueprintRepository struct { + Baseurls *[]string `json:"baseurls,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Filename *string `json:"filename,omitempty"` + Gpgcheck *bool `json:"gpgcheck,omitempty"` + Gpgkeys *[]string `json:"gpgkeys,omitempty"` + Id string `json:"id"` + Metalink *string `json:"metalink,omitempty"` + Mirrorlist *string `json:"mirrorlist,omitempty"` + + // Disables modularity filtering for this repository. + ModuleHotfixes *bool `json:"module_hotfixes,omitempty"` + Name *string `json:"name,omitempty"` + Priority *int `json:"priority,omitempty"` + RepoGpgcheck *bool `json:"repo_gpgcheck,omitempty"` + Sslverify *bool `json:"sslverify,omitempty"` +} + +// BlueprintUser defines model for BlueprintUser. +type BlueprintUser struct { + Description *string `json:"description,omitempty"` + + // Group id to use instead of the default + Gid *int `json:"gid,omitempty"` + + // A list of additional groups to add the user to + Groups *[]string `json:"groups,omitempty"` + + // The user's home directory + Home *string `json:"home,omitempty"` + + // ssh public key + Key *string `json:"key,omitempty"` + Name string `json:"name"` + + // If the password starts with $6$, $5$, or $2b$ it will be stored as + // an encrypted password. Otherwise it will be treated as a plain text + // password. + Password *string `json:"password,omitempty"` + + // Login shell to use + Shell *string `json:"shell,omitempty"` + + // User id to use instead of the default + Uid *int `json:"uid,omitempty"` +} + // CloneComposeBody defines model for CloneComposeBody. type CloneComposeBody interface{} @@ -258,6 +446,7 @@ type ComposeMetadata struct { // ComposeRequest defines model for ComposeRequest. type ComposeRequest struct { + Blueprint *Blueprint `json:"blueprint,omitempty"` Customizations *Customizations `json:"customizations,omitempty"` Distribution string `json:"distribution"` ImageRequest *ImageRequest `json:"image_request,omitempty"` @@ -485,13 +674,27 @@ type FirewallCustomization struct { Ports *[]string `json:"ports,omitempty"` // Firewalld services to enable or disable - Services *struct { - // List of services to disable - Disabled *[]string `json:"disabled,omitempty"` + Services *FirewallServices `json:"services,omitempty"` +} + +// Firewalld services to enable or disable +type FirewallServices struct { + // List of services to disable + Disabled *[]string `json:"disabled,omitempty"` + + // List of services to enable + Enabled *[]string `json:"enabled,omitempty"` +} + +// Bind a list of network sources to a zone to restrict traffic from +// those sources based on the settings of the zone. +type FirewallZones struct { + // name of the zone, if left empty the sources will apply to + // the default zone. + Name *string `json:"name,omitempty"` - // List of services to enable - Enabled *[]string `json:"enabled,omitempty"` - } `json:"services,omitempty"` + // List of sources for the zone + Sources *[]string `json:"sources,omitempty"` } // GCPUploadOptions defines model for GCPUploadOptions. @@ -696,6 +899,24 @@ type OpenSCAPTailoring struct { Unselected *[]string `json:"unselected,omitempty"` } +// Package defines model for Package. +type Package struct { + // Name of the package to install. File globbing is supported, + // eg. 'openssh-*' + Name string `json:"name"` + + // Optional version of the package to install. If left blank the + // latest available version will be used. Wildcards are supported + // eg. '4.11.*' + Version *string `json:"version,omitempty"` +} + +// PackageGroup defines model for PackageGroup. +type PackageGroup struct { + // Package group name + Name string `json:"name"` +} + // PackageMetadata defines model for PackageMetadata. type PackageMetadata struct { Arch string `json:"arch"` @@ -751,6 +972,15 @@ type Repository struct { Rhsm *bool `json:"rhsm,omitempty"` } +// SSHKey defines model for SSHKey. +type SSHKey struct { + // Adds the key to the user's authorized_keys file + Key string `json:"key"` + + // User to configure the ssh key for + User string `json:"user"` +} + // Services defines model for Services. type Services struct { // List of services to disable by default @@ -1101,158 +1331,176 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // 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 0af64f8b37..5a5e1813e3 100644 --- a/internal/cloudapi/v2/openapi.v2.yml +++ b/internal/cloudapi/v2/openapi.v2.yml @@ -748,6 +748,7 @@ components: additionalProperties: false required: - distribution + not: {required: ['customizations', 'blueprint']} properties: distribution: type: string @@ -762,6 +763,8 @@ components: $ref: '#/components/schemas/Customizations' koji: $ref: '#/components/schemas/Koji' + blueprint: + $ref: '#/components/schemas/Blueprint' ImageRequest: additionalProperties: false required: @@ -920,6 +923,49 @@ components: type: integer module_hotfixes: type: boolean + BlueprintRepository: + type: object + required: + - id + properties: + id: + type: string + baseurls: + type: array + items: + type: string + format: uri + example: 'https://cdn.redhat.com/content/dist/rhel8/8/x86_64/baseos/os/' + gpgkeys: + type: array + items: + type: string + metalink: + type: string + format: uri + example: 'https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=x86_64' + mirrorlist: + type: string + format: uri + example: 'http://mirrorlist.centos.org/?release=8-stream&arch=aarch64&repo=BaseOS' + name: + type: string + priority: + type: integer + enabled: + type: boolean + gpgcheck: + type: boolean + repo_gpgcheck: + type: boolean + sslverify: + type: boolean + filename: + type: string + module_hotfixes: + type: boolean + description: | + Disables modularity filtering for this repository. OpenSCAP: type: object required: @@ -929,6 +975,17 @@ components: type: string tailoring: $ref: '#/components/schemas/OpenSCAPTailoring' + BlueprintOpenSCAP: + type: object + required: + - profile_id + properties: + profile_id: + type: string + datastream: + type: string + tailoring: + $ref: '#/components/schemas/OpenSCAPTailoring' OpenSCAPTailoring: type: object properties: @@ -1145,6 +1202,174 @@ components: server_address: type: string format: uri + Blueprint: + type: object + required: + - name + additionalProperties: false + properties: + name: + type: string + description: + type: string + version: + type: string + example: '7.7.70' + description: A semver version number + distro: + type: string + example: 'fedora-39' + description: | + The distribution to use for the compose. If left empty the host + distro will be used. + packages: + type: array + description: Packages to be installed + items: + $ref: '#/components/schemas/Package' + modules: + type: array + description: | + An alias for packages, retained for backwards compatability + items: + $ref: '#/components/schemas/Package' + groups: + type: array + description: Package groups to be installed + items: + $ref: '#/components/schemas/PackageGroup' + containers: + type: array + description: Container images to embed into the final artfact + items: + $ref: '#/components/schemas/Container' + customizations: + $ref: '#/components/schemas/BlueprintCustomizations' + BlueprintCustomizations: + type: object + additionalProperties: false + properties: + hostname: + type: string + description: Configures the hostname + kernel: + $ref: '#/components/schemas/Kernel' + sshkey: + type: array + description: List of ssh keys + items: + $ref: '#/components/schemas/SSHKey' + user: + type: array + description: List of users to create + items: + $ref: '#/components/schemas/BlueprintUser' + group: + type: array + description: List of groups to create + items: + $ref: '#/components/schemas/Group' + timezone: + $ref: '#/components/schemas/Timezone' + locale: + $ref: '#/components/schemas/Locale' + firewall: + $ref: '#/components/schemas/BlueprintFirewall' + services: + $ref: '#/components/schemas/Services' + filesystem: + type: array + description: List of filesystem mountpoints to create + items: + $ref: '#/components/schemas/BlueprintFilesystem' + installation_device: + type: string + description: | + Name of the installation device, currently only useful for the edge-simplified-installer type + example: /dev/sda + partitioning_mode: + type: string + enum: + - raw + - lvm + - auto-lvm + default: auto-lvm + description: | + Select how the disk image will be partitioned. 'auto-lvm' will use raw unless + there are one or more mountpoints in which case it will use LVM. 'lvm' always + uses LVM, even when there are no extra mountpoints. 'raw' uses raw partitions + even when there are one or more mountpoints. + fdo: + $ref: '#/components/schemas/FDO' + openscap: + $ref: '#/components/schemas/BlueprintOpenSCAP' + ignition: + $ref: '#/components/schemas/Ignition' + directories: + type: array + description: Directories to create in the final artifact + items: + $ref: '#/components/schemas/Directory' + files: + type: array + description: Files to create in the final artifact + items: + $ref: '#/components/schemas/BlueprintFile' + repositories: + type: array + description: | + Repositories to write to /etc/yum.repos.d/ in the final image. Note + that these are not used at build time. + items: + $ref: '#/components/schemas/BlueprintRepository' + fips: + type: boolean + description: Enable FIPS mode + SSHKey: + type: object + additionalProperties: false + required: + - user + - key + properties: + user: + type: string + description: User to configure the ssh key for + example: admin + key: + type: string + description: Adds the key to the user's authorized_keys file + example: | + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIASWitkOH4U874EdsBUnytb3bwvRggHlQlbYXl7n10v9 + Package: + type: object + required: + - name + additionalProperties: false + properties: + name: + type: string + example: 'tmux' + description: | + Name of the package to install. File globbing is supported, + eg. 'openssh-*' + version: + type: string + example: '3.3a' + description: | + Optional version of the package to install. If left blank the + latest available version will be used. Wildcards are supported + eg. '4.11.*' + PackageGroup: + type: object + required: + - name + additionalProperties: false + properties: + name: + type: string + example: 'anaconda-tools' + description: Package group name Customizations: type: object additionalProperties: false @@ -1270,22 +1495,59 @@ components: items: type: string services: - type: object - description: Firewalld services to enable or disable - additionalProperties: false - properties: - enabled: - type: array - description: List of services to enable - example: ["ftp", "ntp"] - items: - type: string - disabled: - type: array - description: List of services to disable - example: ["telnet"] - items: - type: string + $ref: '#/components/schemas/FirewallServices' + BlueprintFirewall: + type: object + description: Firewalld configuration + additionalProperties: false + properties: + ports: + type: array + description: List of ports (or port ranges) and protocols to open + example: ["22:tcp", "80:tcp", "imap:tcp"] + items: + type: string + services: + $ref: '#/components/schemas/FirewallServices' + zones: + type: array + items: + $ref: '#/components/schemas/FirewallZones' + FirewallServices: + type: object + description: Firewalld services to enable or disable + additionalProperties: false + properties: + enabled: + type: array + description: List of services to enable + example: ["ftp", "ntp"] + items: + type: string + disabled: + type: array + description: List of services to disable + example: ["telnet"] + items: + type: string + FirewallZones: + type: object + description: | + Bind a list of network sources to a zone to restrict traffic from + those sources based on the settings of the zone. + additionalProperties: false + properties: + name: + type: string + description: | + name of the zone, if left empty the sources will apply to + the default zone. + sources: + type: array + description: List of sources for the zone + items: + type: string + description: [/]||ipset: Directory: type: object description: | @@ -1352,6 +1614,36 @@ components: description: Ensure that the parent directories exist example: true default: false + BlueprintFile: + type: object + description: | + A custom file to create in the final artifact. + required: + - path + properties: + path: + type: string + description: Path to the file + example: '/etc/myfile' + mode: + type: string + description: Permissions string for the file in octal format + example: "0644" + user: + oneOf: + - type: string + - type: integer + description: Owner of the file as a uid or a user name + example: 'root' + group: + oneOf: + - type: string + - type: integer + description: Group of the file as a gid or a group name + example: 'root' + data: + type: string + description: Contents of the file as plain text Filesystem: type: object required: @@ -1365,6 +1657,19 @@ components: x-go-type: uint64 example: 2147483648 description: 'size of the filesystem in bytes' + BlueprintFilesystem: + type: object + required: + - mountpoint + - minsize + properties: + mountpoint: + type: string + example: '/var' + minsize: + x-go-type: uint64 + example: 2147483648 + description: 'size of the filesystem in bytes' OSTree: type: object properties: @@ -1442,6 +1747,45 @@ components: key: type: string example: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrGKErMYi+MMUwuHaRAJmRLoIzRf2qD2dD5z0BTx/6x" + BlueprintUser: + type: object + additionalProperties: false + required: + - name + properties: + name: + type: string + example: "user1" + description: + type: string + password: + type: string + description: | + If the password starts with $6$, $5$, or $2b$ it will be stored as + an encrypted password. Otherwise it will be treated as a plain text + password. + key: + type: string + description: ssh public key + example: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrGKErMYi+MMUwuHaRAJmRLoIzRf2qD2dD5z0BTx/6x" + home: + type: string + description: The user's home directory + shell: + type: string + description: Login shell to use + groups: + type: array + items: + type: string + example: "group1" + description: A list of additional groups to add the user to + uid: + type: integer + description: User id to use instead of the default + gid: + type: integer + description: Group id to use instead of the default Kernel: type: object additionalProperties: false @@ -1557,7 +1901,7 @@ components: type: object additionalProperties: false required: - - name + - name properties: name: type: string