debian-forge-composer/cmd/osbuild-worker/jobimpl-osbuild-koji.go
Simon Steinbeiss cdddc3f81c worker: Properly log successful image builds
This will make counting successful image builds in Splunk and Cloudwatch
much simpler and robust.
2022-02-18 09:27:35 +01:00

178 lines
5.3 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/upload/koji"
"github.com/osbuild/osbuild-composer/internal/worker"
"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
"github.com/sirupsen/logrus"
)
type OSBuildKojiJobImpl struct {
Store string
Output string
KojiServers map[string]koji.GSSAPICredentials
relaxTimeoutFactor uint
}
func (impl *OSBuildKojiJobImpl) kojiUpload(file *os.File, server, directory, filename string) (string, uint64, error) {
transport := koji.CreateKojiTransport(impl.relaxTimeoutFactor)
serverURL, err := url.Parse(server)
if err != nil {
return "", 0, err
}
creds, exists := impl.KojiServers[serverURL.Hostname()]
if !exists {
return "", 0, fmt.Errorf("Koji server has not been configured: %s", serverURL.Hostname())
}
k, err := koji.NewFromGSSAPI(server, &creds, transport)
if err != nil {
return "", 0, err
}
defer func() {
err := k.Logout()
if err != nil {
logrus.Warnf("koji logout failed: %v", err)
}
}()
return k.Upload(file, directory, filename)
}
func validateKojiResult(result *worker.OSBuildKojiJobResult, jobID string) {
logWithId := logrus.WithField("jobId", jobID)
if result.JobError != nil {
logWithId.Errorf("osbuild job failed: %s", result.JobError.Reason)
return
}
// if the job failed, but the JobError is
// nil, we still need to handle this as an error
if result.OSBuildOutput == nil || !result.OSBuildOutput.Success {
reason := "osbuild job was unsuccessful"
logWithId.Errorf("osbuild job failed: %s", reason)
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, reason)
} else {
logWithId.Infof("osbuild-koji job succeeded")
}
}
func (impl *OSBuildKojiJobImpl) Run(job worker.Job) error {
var result worker.OSBuildKojiJobResult
outputDirectory, err := ioutil.TempDir(impl.Output, job.Id().String()+"-*")
if err != nil {
return fmt.Errorf("error creating temporary output directory: %v", err)
}
defer func() {
validateKojiResult(&result, job.Id().String())
// this is necessary for early return errors
err = job.Update(&result)
if err != nil {
logrus.Warnf("Error reporting job result: %v", err)
}
err := os.RemoveAll(outputDirectory)
if err != nil {
logrus.Warnf("Error removing temporary output directory (%s): %v", outputDirectory, err)
}
}()
var args worker.OSBuildKojiJob
err = job.Args(&args)
if err != nil {
return err
}
var initArgs worker.KojiInitJobResult
err = job.DynamicArgs(0, &initArgs)
if err != nil {
return err
}
result.Arch = common.CurrentArch()
result.HostOS, err = distro.GetRedHatRelease()
if err != nil {
return err
}
// In case the manifest is empty, try to get it from dynamic args
if len(args.Manifest) == 0 {
if job.NDynamicArgs() > 1 {
var manifestJR worker.ManifestJobByIDResult
err = job.DynamicArgs(1, &manifestJR)
if err != nil {
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorParsingDynamicArgs, "Error parsing dynamic args")
return err
}
// skip the job if the manifest generation failed
if manifestJR.JobError != nil {
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, "Manifest dependency failed")
return nil
}
args.Manifest = manifestJR.Manifest
if len(args.Manifest) == 0 {
err := fmt.Errorf("Received empty manifest")
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, err.Error())
return err
}
} else {
err := fmt.Errorf("Job has no manifest")
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorManifestDependency, err.Error())
return err
}
}
if initArgs.JobError == nil {
exports := args.Exports
if len(exports) == 0 {
// job did not define exports, likely coming from an older version of composer
// fall back to default "assembler"
exports = []string{"assembler"}
} else if len(exports) > 1 {
// this worker only supports returning one (1) export
err = fmt.Errorf("at most one build artifact can be exported")
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, err.Error())
return err
}
result.OSBuildOutput, err = RunOSBuild(args.Manifest, impl.Store, outputDirectory, exports, os.Stderr)
if err != nil {
return err
}
// NOTE: Currently OSBuild supports multiple exports, but this isn't used
// by any of the image types and it can't be specified during the request.
// Use the first (and presumably only) export for the imagePath.
exportPath := exports[0]
if result.OSBuildOutput.Success {
f, err := os.Open(path.Join(outputDirectory, exportPath, args.ImageName))
if err != nil {
return err
}
result.ImageHash, result.ImageSize, err = impl.kojiUpload(f, args.KojiServer, args.KojiDirectory, args.KojiFilename)
if err != nil {
result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorKojiBuild, err.Error())
}
}
}
// copy pipeline info to the result
result.PipelineNames = args.PipelineNames
err = job.Update(&result)
if err != nil {
return fmt.Errorf("Error reporting job result: %v", err)
}
return nil
}