diff --git a/internal/compose/compose.go b/internal/compose/compose.go new file mode 100644 index 000000000..232f7fa92 --- /dev/null +++ b/internal/compose/compose.go @@ -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 +} diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go new file mode 100644 index 000000000..2fcd5df42 --- /dev/null +++ b/internal/compose/compose_test.go @@ -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()) + } + } +} \ No newline at end of file