package osbuild2 import ( "encoding/json" "fmt" "io" "sort" "github.com/osbuild/osbuild-composer/internal/osbuild1" ) type Result struct { Type string `json:"type"` Success bool `json:"success"` Error json.RawMessage `json:"error,omitempty"` Log map[string]PipelineResult `json:"log"` Metadata map[string]PipelineMetadata `json:"metadata"` } type PipelineResult []StageResult type StageResult struct { ID string `json:"id"` Type string `json:"type"` Output string `json:"output"` Success bool `json:"success,omitempty"` Error string `json:"string,omitempty"` } type PipelineMetadata map[string]StageMetadata type StageMetadata interface { isStageMetadata() } // RawStageMetadata is used to store the metadata from a stage that doesn't // define its own structure type RawStageMetadata json.RawMessage func (RawStageMetadata) isStageMetadata() {} // UnmarshalJSON decodes json-encoded StageResult. // // This method is here only as a workaround for the default value of the // success field, see the comment inside the method. func (sr *StageResult) UnmarshalJSON(data []byte) error { // Create a StageResult-like object with the Success value set to true // before json.Unmarshal is called. If the success field isn't in the // input JSON, the json decoder will not touch it and thus it will still // be true. // // The type alias is needed to prevent recursive calls of this method. type stageResultAlias StageResult stageResultDefault := stageResultAlias{ Success: true, } err := json.Unmarshal(data, &stageResultDefault) if err != nil { return err } *sr = StageResult(stageResultDefault) return nil } func (md *PipelineMetadata) UnmarshalJSON(data []byte) error { var rawPipelineMetadata map[string]json.RawMessage if err := json.Unmarshal(data, &rawPipelineMetadata); err != nil { return err } var pmd PipelineMetadata = make(map[string]StageMetadata) for name, rawStageData := range rawPipelineMetadata { var metadata StageMetadata switch name { case "org.osbuild.rpm": metadata = new(RPMStageMetadata) if err := json.Unmarshal(rawStageData, metadata); err != nil { return err } case "org.osbuild.ostree.commit": metadata = new(OSTreeCommitStageMetadata) if err := json.Unmarshal(rawStageData, metadata); err != nil { return err } default: metadata = RawStageMetadata(rawStageData) } pmd[name] = metadata } *md = pmd return 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 { // Implicit memory alasing doesn't couse any bug in this case /* #nosec G601 */ 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 if resv1.Build != nil { buildResult, buildMetadata := convertStageResults(resv1.Build.Stages) log["build"] = buildResult if len(buildMetadata) > 0 { metadata["build"] = buildMetadata } } // make assembler pipeline from assembler result if resv1.Assembler != nil { 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 if len(resv1.Stages) > 0 { osResult, osMetadata := convertStageResults(resv1.Stages) log["os"] = osResult if len(osMetadata) > 0 { metadata["os"] = osMetadata } } res.Log = log res.Metadata = metadata } func (res *Result) Write(writer io.Writer) error { if res == nil || res.Log == nil { fmt.Fprintf(writer, "The compose result is empty.\n") return nil } // 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(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 := 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) // print structured stage metadata if available if pipelineMD != nil { if md, ok := pipelineMD[stage.Type]; ok { fmt.Fprint(writer, "Metadata:\n") enc := json.NewEncoder(writer) enc.SetIndent("", " ") err := enc.Encode(md) if err != nil { return err } } fmt.Fprint(writer, "\n") } } } 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 }