obsuild2: support additional layers in oci-archive
The org.osbuild.oci-archive stage now supports an arbitrary number of layers on top of the Base layer. The keys for these layers follow the pattern "layer.N" (N = 1, 2, 3, ...). We use a custom marshaller and unmarshaller for the OCIArchiveStageInputs to handle this. The unmarshaller also validates the layer keys to match the pattern in the schema.
This commit is contained in:
parent
dc5e46139a
commit
f107241ee2
2 changed files with 193 additions and 0 deletions
|
|
@ -1,5 +1,13 @@
|
|||
package osbuild2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type OCIArchiveStageOptions struct {
|
||||
// The CPU architecture of the image
|
||||
Architecture string `json:"architecture"`
|
||||
|
|
@ -25,7 +33,10 @@ type OCIArchiveConfig struct {
|
|||
func (OCIArchiveStageOptions) isStageOptions() {}
|
||||
|
||||
type OCIArchiveStageInputs struct {
|
||||
// Base layer for the container
|
||||
Base *OCIArchiveStageInput `json:"base"`
|
||||
// Additional layers in ascending order
|
||||
Layers []OCIArchiveStageInput `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (OCIArchiveStageInputs) isStageInputs() {}
|
||||
|
|
@ -49,3 +60,80 @@ func NewOCIArchiveStage(options *OCIArchiveStageOptions, inputs *OCIArchiveStage
|
|||
Inputs: inputs,
|
||||
}
|
||||
}
|
||||
|
||||
// Custom marshaller for OCIArchiveStageInputs, needed to generate keys of the
|
||||
// form "layer.N", (where N = 1, 2, ...) for the Layers property
|
||||
func (inputs *OCIArchiveStageInputs) MarshalJSON() ([]byte, error) {
|
||||
if inputs == nil {
|
||||
return json.Marshal(inputs)
|
||||
}
|
||||
|
||||
layers := inputs.Layers
|
||||
inputsMap := make(map[string]OCIArchiveStageInput, len(layers)+1)
|
||||
if inputs.Base != nil {
|
||||
inputsMap["base"] = *inputs.Base
|
||||
}
|
||||
|
||||
for idx, input := range layers {
|
||||
key := fmt.Sprintf("layer.%d", idx+1)
|
||||
inputsMap[key] = input
|
||||
}
|
||||
|
||||
return json.Marshal(inputsMap)
|
||||
}
|
||||
|
||||
// Get the sorted keys that match the pattern "layer.N" (for N > 0)
|
||||
func layerKeys(layers map[string]OCIArchiveStageInput) ([]string, error) {
|
||||
keys := make([]string, 0, len(layers))
|
||||
for key := range layers {
|
||||
re := regexp.MustCompile(`layer\.[1-9]\d*`)
|
||||
if key == "base" {
|
||||
continue
|
||||
}
|
||||
if !re.MatchString(key) {
|
||||
return nil, fmt.Errorf("invalid key: %q", key)
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// Custom unmarshaller for OCIArchiveStageInputs, needed to handle keys of the
|
||||
// form "layer.N", (where N = 1, 2, ...) for the Layers property
|
||||
func (inputs *OCIArchiveStageInputs) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if inputs == nil {
|
||||
inputs = new(OCIArchiveStageInputs)
|
||||
}
|
||||
|
||||
inputsMap := make(map[string]OCIArchiveStageInput)
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
|
||||
if err := dec.Decode(&inputsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// "base" layer is required
|
||||
base, ok := inputsMap["base"]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing required key \"base\"")
|
||||
}
|
||||
|
||||
inputs.Base = &base
|
||||
keys, err := layerKeys(inputsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs.Layers = make([]OCIArchiveStageInput, len(inputsMap)-1)
|
||||
for idx, key := range keys {
|
||||
inputs.Layers[idx] = inputsMap[key]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
105
internal/osbuild2/oci_archive_stage_test.go
Normal file
105
internal/osbuild2/oci_archive_stage_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package osbuild2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOCIArchiveStage(t *testing.T) {
|
||||
expectedStage := &Stage{
|
||||
Type: "org.osbuild.oci-archive",
|
||||
Options: &OCIArchiveStageOptions{},
|
||||
Inputs: &OCIArchiveStageInputs{},
|
||||
}
|
||||
actualStage := NewOCIArchiveStage(&OCIArchiveStageOptions{}, &OCIArchiveStageInputs{})
|
||||
assert.Equal(t, expectedStage, actualStage)
|
||||
}
|
||||
|
||||
func TestOCIArchiveInputs(t *testing.T) {
|
||||
exp := `{
|
||||
"base": {
|
||||
"type": "org.osbuild.oci-archive",
|
||||
"origin":"org.osbuild.pipeline",
|
||||
"references": ["name:container-tree"]
|
||||
},
|
||||
"layer.1": {
|
||||
"type": "org.osbuild.tree",
|
||||
"origin": "org.osbuild.pipeline",
|
||||
"references": ["name:container-ostree"]
|
||||
},
|
||||
"layer.2": {
|
||||
"type": "org.osbuild.tree",
|
||||
"origin": "org.osbuild.pipeline",
|
||||
"references": ["name:container-ostree2"]
|
||||
}
|
||||
}`
|
||||
inputs := new(OCIArchiveStageInputs)
|
||||
base := &OCIArchiveStageInput{
|
||||
References: []string{
|
||||
"name:container-tree",
|
||||
},
|
||||
}
|
||||
base.Type = "org.osbuild.oci-archive"
|
||||
base.Origin = "org.osbuild.pipeline"
|
||||
|
||||
layer1 := OCIArchiveStageInput{
|
||||
References: []string{
|
||||
"name:container-ostree",
|
||||
},
|
||||
}
|
||||
layer1.Type = "org.osbuild.tree"
|
||||
layer1.Origin = "org.osbuild.pipeline"
|
||||
layer2 := OCIArchiveStageInput{
|
||||
References: []string{
|
||||
"name:container-ostree2",
|
||||
},
|
||||
}
|
||||
layer2.Type = "org.osbuild.tree"
|
||||
layer2.Origin = "org.osbuild.pipeline"
|
||||
|
||||
inputs.Base = base
|
||||
inputs.Layers = []OCIArchiveStageInput{layer1, layer2}
|
||||
|
||||
data, err := json.Marshal(inputs)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, exp, string(data))
|
||||
|
||||
inputsRead := new(OCIArchiveStageInputs)
|
||||
err = json.Unmarshal([]byte(exp), inputsRead)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, inputs, inputsRead)
|
||||
}
|
||||
|
||||
func TestOCIArchiveInputsErrors(t *testing.T) {
|
||||
noBase := `{
|
||||
"layer.10": {
|
||||
"type": "org.osbuild.tree",
|
||||
"origin": "org.osbuild.pipeline",
|
||||
"references": ["name:container-ostree"]
|
||||
},
|
||||
"layer.2": {
|
||||
"type": "org.osbuild.tree",
|
||||
"origin": "org.osbuild.pipeline",
|
||||
"references": ["name:container-ostree2"]
|
||||
}
|
||||
}`
|
||||
|
||||
inputsRead := new(OCIArchiveStageInputs)
|
||||
assert.Error(t, json.Unmarshal([]byte(noBase), inputsRead))
|
||||
|
||||
invalidKey := `{
|
||||
"base": {
|
||||
"type": "org.osbuild.oci-archive",
|
||||
"origin":"org.osbuild.pipeline",
|
||||
"references": ["name:container-tree"]
|
||||
},
|
||||
"not-a-layer": {
|
||||
"type": "org.osbuild.tree",
|
||||
"origin": "org.osbuild.pipeline",
|
||||
"references": ["name:container-ostree2"]
|
||||
}
|
||||
}`
|
||||
assert.Error(t, json.Unmarshal([]byte(invalidKey), inputsRead))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue