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:
Achilleas Koutsou 2021-09-06 20:09:41 +02:00 committed by Ondřej Budai
parent 9eff6f1e95
commit 10eb0d65a1
2 changed files with 133 additions and 179 deletions

View file

@ -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
}

View file

@ -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
}