package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"time"
"github.com/osbuild/osbuild-composer/internal/upload/koji"
"github.com/osbuild/osbuild-composer/internal/worker"
)
type KojiFinalizeJobImpl struct {
KojiServers map[string]koji.GSSAPICredentials
}
func (impl *KojiFinalizeJobImpl) kojiImport(
server string,
build koji.ImageBuild,
buildRoots []koji.BuildRoot,
images []koji.Image,
directory, token string) error {
// Koji for some reason needs TLS renegotiation enabled.
// Clone the default http transport and enable renegotiation.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
Renegotiation: tls.RenegotiateOnceAsClient,
}
serverURL, err := url.Parse(server)
if err != nil {
return err
}
creds, exists := impl.KojiServers[serverURL.Hostname()]
if !exists {
return fmt.Errorf("Koji server has not been configured: %s", serverURL.Hostname())
}
k, err := koji.NewFromGSSAPI(server, &creds, transport)
if err != nil {
return err
}
defer func() {
err := k.Logout()
if err != nil {
log.Printf("koji logout failed: %v", err)
}
}()
_, err = k.CGImport(build, buildRoots, images, directory, token)
if err != nil {
return fmt.Errorf("Could not import build into koji: %v", err)
}
return nil
}
func (impl *KojiFinalizeJobImpl) kojiFail(server string, buildID int, token string) error {
// Koji for some reason needs TLS renegotiation enabled.
// Clone the default http transport and enable renegotiation.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
Renegotiation: tls.RenegotiateOnceAsClient,
}
serverURL, err := url.Parse(server)
if err != nil {
return err
}
creds, exists := impl.KojiServers[serverURL.Hostname()]
if !exists {
return fmt.Errorf("Koji server has not been configured: %s", serverURL.Hostname())
}
k, err := koji.NewFromGSSAPI(server, &creds, transport)
if err != nil {
return err
}
defer func() {
err := k.Logout()
if err != nil {
log.Printf("koji logout failed: %v", err)
}
}()
return k.CGFailBuild(buildID, token)
}
func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
var args worker.KojiFinalizeJob
err := job.Args(&args)
if err != nil {
return err
}
initArgs, osbuildKojiResults, err := extractDynamicArgs(job)
if err != nil {
return err
}
// Check the dependencies early. Fail the koji build if any of them failed.
if hasFailedDependency(*initArgs, osbuildKojiResults) {
err = impl.kojiFail(args.Server, int(initArgs.BuildID), initArgs.Token)
// Update the status immediately and bail out.
var result worker.KojiFinalizeJobResult
if err != nil {
result.KojiError = err.Error()
}
err = job.Update(&result)
if err != nil {
return fmt.Errorf("Error reporting job result: %v", err)
}
return nil
}
build := koji.ImageBuild{
BuildID: initArgs.BuildID,
TaskID: args.TaskID,
Name: args.Name,
Version: args.Version,
Release: args.Release,
StartTime: int64(args.StartTime),
EndTime: time.Now().Unix(),
}
var buildRoots []koji.BuildRoot
var images []koji.Image
for i, buildArgs := range osbuildKojiResults {
buildRoots = append(buildRoots, koji.BuildRoot{
ID: uint64(i),
Host: koji.Host{
Os: buildArgs.HostOS,
Arch: buildArgs.Arch,
},
ContentGenerator: koji.ContentGenerator{
Name: "osbuild",
Version: "0", // TODO: put the correct version here
},
Container: koji.Container{
Type: "none",
Arch: buildArgs.Arch,
},
Tools: []koji.Tool{},
RPMs: osbuildStagesToRPMs(buildArgs.OSBuildOutput.Build.Stages),
})
images = append(images, koji.Image{
BuildRootID: uint64(i),
Filename: args.KojiFilenames[i],
FileSize: buildArgs.ImageSize,
Arch: buildArgs.Arch,
ChecksumType: "md5",
MD5: buildArgs.ImageHash,
Type: "image",
RPMs: osbuildStagesToRPMs(buildArgs.OSBuildOutput.Stages),
Extra: koji.ImageExtra{
Info: koji.ImageExtraInfo{
Arch: buildArgs.Arch,
},
},
})
}
var result worker.KojiFinalizeJobResult
err = impl.kojiImport(args.Server, build, buildRoots, images, args.KojiDirectory, initArgs.Token)
if err != nil {
result.KojiError = err.Error()
}
err = job.Update(&result)
if err != nil {
return fmt.Errorf("Error reporting job result: %v", err)
}
return nil
}
// Extracts dynamic args of the koji-finalize job. Returns an error if they
// cannot be unmarshalled.
func extractDynamicArgs(job worker.Job) (*worker.KojiInitJobResult, []worker.OSBuildKojiJobResult, error) {
var kojiInitResult worker.KojiInitJobResult
err := job.DynamicArgs(0, &kojiInitResult)
if err != nil {
return nil, nil, err
}
osbuildKojiResults := make([]worker.OSBuildKojiJobResult, job.NDynamicArgs()-1)
for i := 1; i < job.NDynamicArgs(); i++ {
err = job.DynamicArgs(i, &osbuildKojiResults[i-1])
if err != nil {
return nil, nil, err
}
}
return &kojiInitResult, osbuildKojiResults, nil
}
// Returns true if any of koji-finalize dependencies failed.
func hasFailedDependency(kojiInitResult worker.KojiInitJobResult, osbuildKojiResults []worker.OSBuildKojiJobResult) bool {
if kojiInitResult.KojiError != "" {
return true
}
for _, r := range osbuildKojiResults {
if !r.OSBuildOutput.Success || r.KojiError != "" {
return true
}
}
return false
}