osbuild2: convert from osbuild1 results
Convert osbuild1.Result{} to osbuild2.Result{}.
For the Metadata objects, it assumes they are directly convertible: the
stage metadata structs have the same members.
The old conversion code from v2 to v1 is removed and the equivalent
conversion logic is moved to osbuild2:
- version detection based on stub
- custom unmarshaller that calls conversion function if v1 result is
detected
Signed-off-by: Achilleas Koutsou <achilleas@koutsou.net>
This commit is contained in:
parent
9eff6f1e95
commit
10eb0d65a1
2 changed files with 133 additions and 179 deletions
|
|
@ -4,10 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/osbuild2"
|
||||
)
|
||||
|
||||
type StageResult struct {
|
||||
|
|
@ -130,171 +127,3 @@ func (cr *Result) Write(writer io.Writer) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isV2Result returns true if data contains a json-encoded osbuild result
|
||||
// in version 2 schema.
|
||||
//
|
||||
// It detects the schema version by checking if the decoded json contains
|
||||
// a "type" field at the top-level.
|
||||
//
|
||||
// error is non-nil when data isn't a json-encoded object.
|
||||
func isV2Result(data []byte) (bool, error) {
|
||||
var v2ResultStub struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &v2ResultStub)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return v2ResultStub.Type != "", nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes json-encoded data into a Result struct.
|
||||
//
|
||||
// Note that this function is smart and if a result from manifest v2 is given,
|
||||
// it detects it and converts it to a result like it would be returned for
|
||||
// manifest v1. This conversion is always lossy.
|
||||
//
|
||||
// TODO: We might want to get rid of the smart behaviour and make this method
|
||||
// dumb again.
|
||||
func (cr *Result) UnmarshalJSON(data []byte) error {
|
||||
// detect if the input is v2 result
|
||||
v2Result, err := isV2Result(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v2Result {
|
||||
// do the best-effort conversion from v2
|
||||
var crv2 osbuild2.Result
|
||||
|
||||
// NOTE: Using plain (non-strict) Unmarshal here. The format of the new
|
||||
// osbuild output schema is not yet fixed and is likely to change, so
|
||||
// disallowing unknown fields will likely cause failures in the near future.
|
||||
if err := json.Unmarshal(data, &crv2); err != nil {
|
||||
return err
|
||||
}
|
||||
cr.fromV2(crv2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, unmarshal using a type alias to prevent recursive calls
|
||||
// of this method.
|
||||
type resultAlias Result
|
||||
var crv1 resultAlias
|
||||
err = json.Unmarshal(data, &crv1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*cr = Result(crv1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert new OSBuild v2 format result into a v1 by copying the most useful
|
||||
// values:
|
||||
// - Compose success status
|
||||
// - Output of Stages (Log) as flattened list of v1 StageResults
|
||||
func (cr *Result) fromV2(crv2 osbuild2.Result) {
|
||||
cr.Success = crv2.Success
|
||||
// Empty build and assembler results for new types of jobs
|
||||
cr.Build = new(buildResult)
|
||||
cr.Assembler = new(StageResult)
|
||||
|
||||
// crv2.Log contains a map of pipelines. Unfortunately, Go doesn't
|
||||
// preserve the order of keys in a map. See:
|
||||
// https://github.com/golang/go/issues/27179
|
||||
//
|
||||
// I think it makes sense for this function to always return
|
||||
// a well-defined output, therefore we need to invent an ordering
|
||||
// for pipeline results. Otherwise, the ordering is basically random.
|
||||
//
|
||||
// The following lines convert the map of pipeline results to an array
|
||||
// of pipeline results. In the last step, the array is sorted by
|
||||
// the pipeline name. This isn't ideal but at least it's predictable.
|
||||
//
|
||||
// See: https://github.com/osbuild/osbuild/issues/619
|
||||
type pipelineResult struct {
|
||||
pipelineName string
|
||||
stageResults []osbuild2.StageResult
|
||||
}
|
||||
|
||||
var pipelineResults []pipelineResult
|
||||
|
||||
for pname, stageResults := range crv2.Log {
|
||||
pipelineResults = append(pipelineResults, pipelineResult{pipelineName: pname, stageResults: stageResults})
|
||||
}
|
||||
|
||||
// Sort the pipelineResult array by the pipeline name to ensure a stable order.
|
||||
sort.Slice(pipelineResults, func(i, j int) bool {
|
||||
return pipelineResults[i].pipelineName < pipelineResults[j].pipelineName
|
||||
})
|
||||
|
||||
v2metadata := crv2.Metadata
|
||||
// convert all stages logs from all pipelines into v1 StageResult objects
|
||||
for _, pr := range pipelineResults {
|
||||
pipelineMetadata := v2metadata[pr.pipelineName]
|
||||
for idx, stage := range pr.stageResults {
|
||||
stageMetadataV2 := pipelineMetadata[stage.Type]
|
||||
stageMetadata, _ := convertStageMetadata(stageMetadataV2, stage.Type)
|
||||
stageResult := StageResult{
|
||||
// Create uniquely identifiable name for the stage:
|
||||
// <pipeline name>:<stage index>-<stage type>
|
||||
Name: fmt.Sprintf("%s:%d-%s", pr.pipelineName, idx, stage.Type),
|
||||
Success: stage.Success,
|
||||
Output: stage.Output,
|
||||
Metadata: stageMetadata,
|
||||
}
|
||||
cr.Stages = append(cr.Stages, stageResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func convertStageMetadata(v2md osbuild2.StageMetadata, stageType string) (StageMetadata, error) {
|
||||
if v2md == nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch metadata := v2md.(type) {
|
||||
case *osbuild2.RPMStageMetadata:
|
||||
packages := make([]RPMPackageMetadata, len(metadata.Packages))
|
||||
for idx, pkg := range metadata.Packages {
|
||||
packages[idx] = RPMPackageMetadata{
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
Release: pkg.Release,
|
||||
Epoch: pkg.Epoch,
|
||||
Arch: pkg.Arch,
|
||||
SigMD5: pkg.SigMD5,
|
||||
SigPGP: pkg.SigPGP,
|
||||
SigGPG: pkg.SigGPG,
|
||||
}
|
||||
}
|
||||
return RPMStageMetadata{Packages: packages}, nil
|
||||
case *osbuild2.OSTreeCommitStageMetadata:
|
||||
v2compose := metadata.Compose
|
||||
commitMetadata := OSTreeCommitStageMetadata{
|
||||
Compose: OSTreeCommitStageMetadataCompose{
|
||||
Ref: v2compose.Ref,
|
||||
OSTreeNMetadataTotal: v2compose.OSTreeNMetadataTotal,
|
||||
OSTreeNMetadataWritten: v2compose.OSTreeNMetadataWritten,
|
||||
OSTreeNContentTotal: v2compose.OSTreeNContentTotal,
|
||||
OSTreeNContentWritten: v2compose.OSTreeNContentWritten,
|
||||
OSTreeNCacheHits: v2compose.OSTreeNCacheHits,
|
||||
OSTreeContentBytesWritten: v2compose.OSTreeContentBytesWritten,
|
||||
OSTreeCommit: v2compose.OSTreeCommit,
|
||||
OSTreeContentChecksum: v2compose.OSTreeContentChecksum,
|
||||
OSTreeTimestamp: v2compose.OSTreeTimestamp,
|
||||
RPMOSTreeInputHash: v2compose.RPMOSTreeInputHash,
|
||||
},
|
||||
}
|
||||
return commitMetadata, nil
|
||||
}
|
||||
|
||||
// any other type, return raw
|
||||
raw, err := json.Marshal(v2md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return RawStageMetadata(raw), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/osbuild1"
|
||||
)
|
||||
|
||||
type PipelineResult []StageResult
|
||||
|
|
@ -55,7 +57,7 @@ func (sr *StageResult) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (md *PipelineMetadata) UnmarshalJSON(data []byte) error {
|
||||
func (md PipelineMetadata) UnmarshalJSON(data []byte) error {
|
||||
var rawPipelineMetadata map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawPipelineMetadata); err != nil {
|
||||
return err
|
||||
|
|
@ -79,7 +81,7 @@ func (md *PipelineMetadata) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
pmd[name] = metadata
|
||||
}
|
||||
*md = pmd
|
||||
md = pmd
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -91,24 +93,125 @@ type Result struct {
|
|||
Metadata map[string]PipelineMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func (cr *Result) Write(writer io.Writer) error {
|
||||
if cr.Log == nil {
|
||||
func (res *Result) UnmarshalJSON(data []byte) error {
|
||||
// detect if the input is v1 result
|
||||
if v1Result, err := isV1Result(data); err != nil {
|
||||
return err
|
||||
} else if v1Result {
|
||||
var resv1 osbuild1.Result
|
||||
if err := json.Unmarshal(data, &resv1); err != nil {
|
||||
return err
|
||||
}
|
||||
res.fromV1(resv1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, unmarshal using a type alias to prevent recursive calls to
|
||||
// this method
|
||||
type resultAlias Result
|
||||
var resv2 resultAlias
|
||||
if err := json.Unmarshal(data, &resv2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*res = Result(resv2)
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertStageResult(sr1 *osbuild1.StageResult) (*StageResult, StageMetadata) {
|
||||
sr := &StageResult{
|
||||
ID: "",
|
||||
Type: sr1.Name,
|
||||
Output: sr1.Output,
|
||||
Success: sr1.Success,
|
||||
Error: "",
|
||||
}
|
||||
|
||||
var md StageMetadata = nil
|
||||
if sr1.Metadata != nil {
|
||||
switch md1 := sr1.Metadata.(type) {
|
||||
case *osbuild1.RPMStageMetadata:
|
||||
rpmmd := new(RPMStageMetadata)
|
||||
rpmmd.Packages = make([]RPMPackageMetadata, len(md1.Packages))
|
||||
for idx, pkg := range md1.Packages {
|
||||
rpmmd.Packages[idx] = RPMPackageMetadata(pkg)
|
||||
}
|
||||
md = rpmmd
|
||||
case *osbuild1.OSTreeCommitStageMetadata:
|
||||
commitmd := new(OSTreeCommitStageMetadata)
|
||||
commitmd.Compose = OSTreeCommitStageMetadataCompose(md1.Compose)
|
||||
md = commitmd
|
||||
}
|
||||
|
||||
}
|
||||
return sr, md
|
||||
}
|
||||
|
||||
func convertStageResults(v1Stages []osbuild1.StageResult) (PipelineResult, PipelineMetadata) {
|
||||
result := make([]StageResult, len(v1Stages))
|
||||
metadata := make(map[string]StageMetadata)
|
||||
for idx, srv1 := range v1Stages {
|
||||
stageResult, stageMetadata := convertStageResult(&srv1)
|
||||
result[idx] = *stageResult
|
||||
if stageMetadata != nil {
|
||||
metadata[stageResult.Type] = stageMetadata
|
||||
}
|
||||
}
|
||||
return result, metadata
|
||||
}
|
||||
|
||||
func (res *Result) fromV1(resv1 osbuild1.Result) {
|
||||
res.Success = resv1.Success
|
||||
res.Type = "result"
|
||||
|
||||
log := make(map[string]PipelineResult)
|
||||
metadata := make(map[string]PipelineMetadata)
|
||||
|
||||
// make build pipeline from build result
|
||||
buildResult, buildMetadata := convertStageResults(resv1.Build.Stages)
|
||||
log["build"] = buildResult
|
||||
if len(buildMetadata) > 0 {
|
||||
metadata["build"] = buildMetadata
|
||||
}
|
||||
|
||||
// make assembler pipeline from assembler result
|
||||
assemblerResult, assemblerMetadata := convertStageResult(resv1.Assembler)
|
||||
log["assembler"] = []StageResult{*assemblerResult}
|
||||
if assemblerMetadata != nil {
|
||||
metadata["assembler"] = map[string]StageMetadata{
|
||||
resv1.Assembler.Name: assemblerMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// make os pipeline from main stage results
|
||||
osResult, osMetadata := convertStageResults(resv1.Stages)
|
||||
log["os"] = osResult
|
||||
if len(buildMetadata) > 0 {
|
||||
metadata["os"] = osMetadata
|
||||
}
|
||||
|
||||
res.Log = log
|
||||
res.Metadata = metadata
|
||||
}
|
||||
|
||||
func (res *Result) Write(writer io.Writer) error {
|
||||
if res.Log == nil {
|
||||
fmt.Fprintf(writer, "The compose result is empty.\n")
|
||||
}
|
||||
|
||||
// The pipeline results don't have a stable order
|
||||
// (see https://github.com/golang/go/issues/27179)
|
||||
// Sort based on pipeline name to have a stable print order
|
||||
pipelineNames := make([]string, 0, len(cr.Log))
|
||||
for name := range cr.Log {
|
||||
pipelineNames := make([]string, 0, len(res.Log))
|
||||
for name := range res.Log {
|
||||
pipelineNames = append(pipelineNames, name)
|
||||
}
|
||||
sort.Strings(pipelineNames)
|
||||
|
||||
for _, pipelineName := range pipelineNames {
|
||||
fmt.Fprintf(writer, "Pipeline %s\n", pipelineName)
|
||||
pipelineMD := cr.Metadata[pipelineName]
|
||||
for _, stage := range cr.Log[pipelineName] {
|
||||
pipelineMD := res.Metadata[pipelineName]
|
||||
for _, stage := range res.Log[pipelineName] {
|
||||
fmt.Fprintf(writer, "Stage %s\n", stage.Type)
|
||||
fmt.Fprintf(writer, "Output:\n%s\n", stage.Output)
|
||||
|
||||
|
|
@ -130,3 +233,25 @@ func (cr *Result) Write(writer io.Writer) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isV1Result returns true if data contains a json-encoded osbuild result
|
||||
// in version 1 schema.
|
||||
//
|
||||
// It detects the schema version by checking if the decoded json contains at
|
||||
// least one of the three top-level result objects: Build, Stages, or Assembler
|
||||
//
|
||||
// error is non-nil when data isn't a json-encoded object.
|
||||
func isV1Result(data []byte) (bool, error) {
|
||||
var v1ResultStub struct {
|
||||
Build interface{} `json:"build"`
|
||||
Stages interface{} `json:"stages"`
|
||||
Assembler interface{} `json:"assembler"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &v1ResultStub)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return v1ResultStub.Build != nil || v1ResultStub.Stages != nil || v1ResultStub.Assembler != nil, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue