diff --git a/internal/osbuild1/result.go b/internal/osbuild1/result.go index 1f17ed51e..15616bcef 100644 --- a/internal/osbuild1/result.go +++ b/internal/osbuild1/result.go @@ -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: - // :- - 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 -} diff --git a/internal/osbuild2/result.go b/internal/osbuild2/result.go index 9d0479269..fc76b65c5 100644 --- a/internal/osbuild2/result.go +++ b/internal/osbuild2/result.go @@ -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 +}