compose: refactor compose related structure into its own package

The store package is getting too big and convoluted, this new package
will help with separation of the compose logic from our current
implementation of the store.

The current implementation will be removed in following commits.
This commit is contained in:
Martin Sehnoutka 2020-02-10 09:28:12 +01:00 committed by Ondřej Budai
parent 62d186cd1b
commit eb6c0f6fce
2 changed files with 297 additions and 0 deletions

172
internal/compose/compose.go Normal file
View file

@ -0,0 +1,172 @@
// Package compose encapsulates the concept of a compose. It is a separate module from common, because it includes
// target which in turn includes common and thus it would create a cyclic dependency, which is forbidden in golang.
package compose
import (
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/pipeline"
"github.com/osbuild/osbuild-composer/internal/target"
"time"
)
type StateTransitionError struct {
message string
}
func (ste *StateTransitionError) Error() string {
return ste.message
}
// Image represents the image resulting from a compose.
type Image struct {
Path string
Mime string
Size uint64
}
// ImageBuild represents a single image build inside a compose
type ImageBuild struct {
Id int `json:"id"`
Distro common.Distribution `json:"distro"`
QueueStatus common.ImageBuildState `json:"queue_status"`
ImageType common.ImageType `json:"image_type"`
Pipeline *pipeline.Pipeline `json:"pipeline"`
Targets []*target.Target `json:"targets"`
JobCreated time.Time `json:"job_created"`
JobStarted time.Time `json:"job_started"`
JobFinished time.Time `json:"job_finished"`
Image *Image `json:"image"`
Size uint64 `json:"size"`
}
// DeepCopy creates a copy of the ImageBuild structure
func (ib *ImageBuild) DeepCopy() ImageBuild {
var newImagePtr *Image = nil
if ib.Image != nil {
imageCopy := *ib.Image
newImagePtr = &imageCopy
}
var newPipelinePtr *pipeline.Pipeline = nil
if ib.Pipeline != nil {
pipelineCopy := *ib.Pipeline
newPipelinePtr = &pipelineCopy
}
var newTargets []*target.Target
for _, t := range ib.Targets {
newTarget := *t
newTargets = append(newTargets, &newTarget)
}
// Create new image build struct
return ImageBuild{
Distro: ib.Distro,
QueueStatus: ib.QueueStatus,
ImageType: ib.ImageType,
Pipeline: newPipelinePtr,
Targets: newTargets,
JobCreated: ib.JobCreated,
JobStarted: ib.JobStarted,
JobFinished: ib.JobFinished,
Image: newImagePtr,
}
}
// A Compose represent the task of building a set of images from a single blueprint.
// It contains all the information necessary to generate the inputs for the job, as
// well as the job's state.
type Compose struct {
Blueprint *blueprint.Blueprint `json:"blueprint"`
ImageBuilds []ImageBuild `json:"image_builds"`
}
// DeepCopy creates a copy of the Compose structure
func (c *Compose) DeepCopy() Compose {
var newBpPtr *blueprint.Blueprint = nil
if c.Blueprint != nil {
bpCopy := *c.Blueprint
newBpPtr = &bpCopy
}
newImageBuilds := []ImageBuild{}
for _, ib := range c.ImageBuilds {
newImageBuilds = append(newImageBuilds, ib.DeepCopy())
}
return Compose{
Blueprint: newBpPtr,
ImageBuilds: newImageBuilds,
}
}
func anyImageBuild(fn func(common.ImageBuildState) bool, list []common.ImageBuildState) bool {
acc := false
for _, i := range list {
if fn(i) {
acc = true
}
}
return acc
}
func allImageBuilds(fn func(common.ImageBuildState) bool, list []common.ImageBuildState) bool {
acc := true
for _, i := range list {
if !fn(i) {
acc = false
}
}
return acc
}
// GetState returns a state of the whole compose which is derived from the states of
// individual image builds inside the compose
func (c *Compose) GetState() common.ComposeState {
var imageBuildsStates []common.ImageBuildState
for _, ib := range c.ImageBuilds {
imageBuildsStates = append(imageBuildsStates, ib.QueueStatus)
}
// In case all states are the same
if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBWaiting }, imageBuildsStates) {
return common.CWaiting
}
if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFinished }, imageBuildsStates) {
return common.CFinished
}
if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFailed }, imageBuildsStates) {
return common.CFailed
}
// In case the states are mixed
// TODO: can this condition be removed because it is already covered by the default?
if anyImageBuild(func(ib common.ImageBuildState) bool { return ib == common.IBRunning }, imageBuildsStates) {
return common.CRunning
}
if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFailed || ib == common.IBFinished }, imageBuildsStates) {
return common.CFailed
}
// Default value
return common.CRunning
}
// UpdateState changes a state of a single image build inside the Compose
func (c *Compose) UpdateState(imageBuildId int, newState common.ImageBuildState) error {
switch newState {
case common.IBWaiting:
return &StateTransitionError{"image build cannot be moved into waiting state"}
case common.IBRunning:
if c.ImageBuilds[imageBuildId].QueueStatus == common.IBWaiting || c.ImageBuilds[imageBuildId].QueueStatus == common.IBRunning {
c.ImageBuilds[imageBuildId].QueueStatus = newState
} else {
return &StateTransitionError{"only waiting image build can be transitioned into running state"}
}
case common.IBFinished, common.IBFailed:
if c.ImageBuilds[imageBuildId].QueueStatus == common.IBRunning {
c.ImageBuilds[imageBuildId].QueueStatus = newState
for _, t := range c.ImageBuilds[imageBuildId].Targets {
t.Status = newState
}
} else {
return &StateTransitionError{"only running image build can be transitioned into finished or failed state"}
}
default:
return &StateTransitionError{"invalid state"}
}
return nil
}

View file

@ -0,0 +1,125 @@
package compose
import (
"github.com/osbuild/osbuild-composer/internal/common"
"testing"
)
func TestGetState(t *testing.T) {
cases := []struct{
compose Compose
expecedStatus common.ComposeState
}{
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBWaiting},
},
},
expecedStatus: common.CWaiting,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBRunning},
},
},
expecedStatus: common.CRunning,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBFailed},
},
},
expecedStatus: common.CFailed,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBFinished},
},
},
expecedStatus: common.CFinished,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBWaiting},
{QueueStatus: common.IBWaiting},
},
},
expecedStatus: common.CWaiting,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBWaiting},
{QueueStatus: common.IBRunning},
},
},
expecedStatus: common.CRunning,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBRunning},
{QueueStatus: common.IBRunning},
},
},
expecedStatus: common.CRunning,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBRunning},
{QueueStatus: common.IBFailed},
},
},
expecedStatus: common.CRunning,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBWaiting},
{QueueStatus: common.IBFailed},
},
},
expecedStatus: common.CRunning,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBFailed},
{QueueStatus: common.IBFailed},
},
},
expecedStatus: common.CFailed,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBFinished},
{QueueStatus: common.IBFinished},
},
},
expecedStatus: common.CFinished,
},
{
compose: Compose{
ImageBuilds: []ImageBuild{
{QueueStatus: common.IBFinished},
{QueueStatus: common.IBFailed},
},
},
expecedStatus: common.CFailed,
},
}
for n, c := range cases {
got := c.compose.GetState()
wanted := c.expecedStatus
if got != wanted {
t.Error("Compose", n, "should be in", wanted.ToString(), "state, but it is:", got.ToString())
}
}
}