From eb6c0f6fcee43c9bbe373e25cb5a07e044db8ad0 Mon Sep 17 00:00:00 2001 From: Martin Sehnoutka Date: Mon, 10 Feb 2020 09:28:12 +0100 Subject: [PATCH] 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. --- internal/compose/compose.go | 172 +++++++++++++++++++++++++++++++ internal/compose/compose_test.go | 125 ++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 internal/compose/compose.go create mode 100644 internal/compose/compose_test.go 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