Skip to content

Commit

Permalink
osbuild: add ostree.deploy.container stage
Browse files Browse the repository at this point in the history
Creates an org.osbuild.ostree.deploy.container stage.
Validates the options with a regular expression that matches the schema.
Takes a container as input.  Validates the length of the input
references to be exactly 1.

Also added tests for the validators for options and inputs.

See osbuild/osbuild#1402
  • Loading branch information
achilleas-k committed Nov 27, 2023
1 parent 0464233 commit 6dcaf36
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 0 deletions.
75 changes: 75 additions & 0 deletions pkg/osbuild/ostree_deploy_container_stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package osbuild

import (
"fmt"
"regexp"
)

// adapted from the osbuild stage schema - keep in sync if it ever changes
const ostreeContainerTargetImgrefRegex = "^(ostree-remote-registry|ostree-image-signed|ostree-unverified-registry):.*$"

// Options for the org.osbuild.ostree.deploy.container stage.
type OSTreeDeployContainerStageOptions struct {

// Name of the stateroot to be used in the deployment
OsName string `json:"osname"`

// Additional kernel command line options
KernelOpts []string `json:"kernel_opts,omitempty"`

// Image ref used as the source of truth for updates
TargetImgref string `json:"target_imgref"`

// Identifier to locate the root file system (uuid or label)
Rootfs *Rootfs `json:"rootfs,omitempty"`

// Mount points of the final file system
Mounts []string `json:"mounts,omitempty"`
}

func (OSTreeDeployContainerStageOptions) isStageOptions() {}

func (options OSTreeDeployContainerStageOptions) validate() error {
if options.OsName == "" {
return fmt.Errorf("osname is required")
}

exp := regexp.MustCompile(ostreeContainerTargetImgrefRegex)
if !exp.MatchString(options.TargetImgref) {
return fmt.Errorf("'target_imgref' %q doesn't conform to schema (%s)", options.TargetImgref, exp.String())
}
return nil
}

type OSTreeDeployContainerInputs struct {
Images ContainersInput `json:"images"`
}

func (OSTreeDeployContainerInputs) isStageInputs() {}

func (inputs OSTreeDeployContainerInputs) validate() error {
if inputs.Images.References == nil {
return fmt.Errorf("stage requires exactly 1 input container (got nil References)")
}
if ncontainers := inputs.Images.References.Len(); ncontainers != 1 {
return fmt.Errorf("stage requires exactly 1 input container (got %d)", ncontainers)
}
return nil
}

func NewOSTreeDeployContainerStage(options *OSTreeDeployContainerStageOptions, images ContainersInput) *Stage {
if err := options.validate(); err != nil {
panic(err)
}
inputs := OSTreeDeployContainerInputs{
Images: images,
}
if err := inputs.validate(); err != nil {
panic(err)
}
return &Stage{
Type: "org.osbuild.ostree.deploy.container",
Options: options,
Inputs: inputs,
}
}
157 changes: 157 additions & 0 deletions pkg/osbuild/ostree_deploy_container_stage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package osbuild

import (
"testing"

"github.com/google/uuid"
"github.com/osbuild/images/pkg/container"
"github.com/stretchr/testify/assert"
)

func TestOSTreeDeployContainersStageOptionsValidate(t *testing.T) {
// options are validated first, so this doesn't necessarily need to be
// valid, but we might change the order at some point.
validInputs := NewContainersInputForSources([]container.Spec{
{
ImageID: "id-0",
Source: "registry.example.org/reg/img",
},
})

type testCase struct {
options OSTreeDeployContainerStageOptions
valid bool
}

testCases := map[string]testCase{
"empty": {
options: OSTreeDeployContainerStageOptions{},
valid: false,
},
"minimal": {
options: OSTreeDeployContainerStageOptions{
OsName: "default",
TargetImgref: "ostree-remote-registry:example.org/registry/image",
},
valid: true,
},
"no-target": {
options: OSTreeDeployContainerStageOptions{
OsName: "os",
},
valid: false,
},
"no-os": {
options: OSTreeDeployContainerStageOptions{
TargetImgref: "ostree-image-unverified-registry:example.org/registry/image",
},
valid: false,
},
"bad-target": {
options: OSTreeDeployContainerStageOptions{
OsName: "os",
TargetImgref: "bad",
},
valid: false,
},
"full": {
options: OSTreeDeployContainerStageOptions{
OsName: "default",
KernelOpts: []string{},
TargetImgref: "ostree-image-signed:example.org/registry/image",
Rootfs: &Rootfs{
// defining both is redundant but not invalid
Label: "root",
UUID: uuid.New().String(),
},
Mounts: []string{"/data"},
},
valid: true,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
if tc.valid {
assert.NoError(tc.options.validate())
assert.NotPanics(func() { NewOSTreeDeployContainerStage(&tc.options, validInputs) })
} else {
assert.Error(tc.options.validate())
assert.NotPanics(func() { NewOSTreeDeployContainerStage(&tc.options, validInputs) })
}
})
}

}

func TestOSTreeDeployContainersStageInputsValidate(t *testing.T) {
validOptions := &OSTreeDeployContainerStageOptions{
OsName: "default",
TargetImgref: "ostree-remote-registry:example.org/registry/image",
}

type testCase struct {
inputs OSTreeDeployContainerInputs
valid bool
}

testCases := map[string]testCase{
"empty": {
inputs: OSTreeDeployContainerInputs{},
valid: false,
},
"nil": {
inputs: OSTreeDeployContainerInputs{
Images: ContainersInput{
References: nil,
},
},
valid: false,
},
"zero": {
inputs: OSTreeDeployContainerInputs{
Images: NewContainersInputForSources([]container.Spec{}),
},
valid: false,
},
"one": {
inputs: OSTreeDeployContainerInputs{
Images: NewContainersInputForSources([]container.Spec{
{
ImageID: "id-0",
Source: "registry.example.org/reg/img",
},
}),
},
valid: true,
},
"two": {
inputs: OSTreeDeployContainerInputs{
Images: NewContainersInputForSources([]container.Spec{
{
ImageID: "id-1",
Source: "registry.example.org/reg/img-one",
},
{
ImageID: "id-2",
Source: "registry.example.org/reg/img-two",
},
}),
},
valid: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
if tc.valid {
assert.NoError(tc.inputs.validate())
assert.NotPanics(func() { NewOSTreeDeployContainerStage(validOptions, tc.inputs.Images) })
} else {
assert.Error(tc.inputs.validate())
assert.Panics(func() { NewOSTreeDeployContainerStage(validOptions, tc.inputs.Images) })
}
})
}
}

0 comments on commit 6dcaf36

Please sign in to comment.