Repository: osbuild/osbuild-composer
Pull request #464 (2020-04-03 18:07:12)

Fix problem with blank repo source


Patch:
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index aeef105dd..dd8ddf93f 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -6,12 +6,16 @@ import (
 	"errors"
 	"flag"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"log"
 	"os"
+	"path"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
+	"github.com/osbuild/osbuild-composer/internal/target"
+	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
 )
 
 type connectionConfig struct {
@@ -43,26 +47,85 @@ func createTLSConfig(config *connectionConfig) (*tls.Config, error) {
 	}, nil
 }
 
-func handleJob(client *jobqueue.Client) error {
-	fmt.Println("Waiting for a new job...")
-	job, err := client.AddJob()
-	if err != nil {
-		return err
+type TargetsError struct {
+	Errors []error
+}
+
+func (e *TargetsError) Error() string {
+	errString := fmt.Sprintf("%d target(s) errored:\n", len(e.Errors))
+
+	for _, err := range e.Errors {
+		errString += err.Error() + "\n"
 	}
 
-	err = client.UpdateJob(job, common.IBRunning, nil)
+	return errString
+}
+
+func RunJob(job *jobqueue.Job, uploadFunc func(*jobqueue.Job, io.Reader) error) (*common.ComposeResult, error) {
+	tmpStore, err := ioutil.TempDir("/var/tmp", "osbuild-store")
 	if err != nil {
-		return err
+		return nil, fmt.Errorf("error setting up osbuild store: %v", err)
 	}
+	// FIXME: how to handle errors in defer?
+	defer os.RemoveAll(tmpStore)
 
-	fmt.Printf("Running job %s\n", job.ID.String())
-	result, err := job.Run(client)
+	result, err := RunOSBuild(job.Manifest, tmpStore, os.Stderr)
 	if err != nil {
-		log.Printf("  Job failed: %v", err)
-		return client.UpdateJob(job, common.IBFailed, result)
+		return nil, fmt.Errorf("osbuild error: %v", err)
 	}
 
-	return client.UpdateJob(job, common.IBFinished, result)
+	var r []error
+
+	for _, t := range job.Targets {
+		switch options := t.Options.(type) {
+		case *target.LocalTargetOptions:
+			f, err := os.Open(path.Join(tmpStore, "refs", result.OutputID, options.Filename))
+			if err != nil {
+				r = append(r, err)
+				continue
+			}
+
+			err = uploadFunc(job, f)
+			if err != nil {
+				r = append(r, err)
+				continue
+			}
+
+		case *target.AWSTargetOptions:
+
+			a, err := awsupload.New(options.Region, options.AccessKeyID, options.SecretAccessKey)
+			if err != nil {
+				r = append(r, err)
+				continue
+			}
+
+			if options.Key == "" {
+				options.Key = job.ComposeID.String()
+			}
+
+			_, err = a.Upload(path.Join(tmpStore, "refs", result.OutputID, options.Filename), options.Bucket, options.Key)
+			if err != nil {
+				r = append(r, err)
+				continue
+			}
+
+			/* TODO: communicate back the AMI */
+			_, err = a.Register(t.ImageName, options.Bucket, options.Key)
+			if err != nil {
+				r = append(r, err)
+				continue
+			}
+		case *target.AzureTargetOptions:
+		default:
+			r = append(r, fmt.Errorf("invalid target type"))
+		}
+	}
+
+	if len(r) > 0 {
+		return result, &TargetsError{r}
+	}
+
+	return result, nil
 }
 
 func main() {
@@ -99,8 +162,26 @@ func main() {
 	}
 
 	for {
-		if err := handleJob(client); err != nil {
-			log.Fatalf("Failed to handle job: " + err.Error())
+		fmt.Println("Waiting for a new job...")
+		job, err := client.AddJob()
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		fmt.Printf("Running job %s\n", job.ComposeID.String())
+
+		var status common.ImageBuildState
+		result, err := RunJob(job, client.UploadImage)
+		if err != nil {
+			log.Printf("  Job failed: %v", err)
+			status = common.IBFailed
+		} else {
+			status = common.IBFinished
+		}
+
+		err = client.UpdateJob(job, status, result)
+		if err != nil {
+			log.Fatalf("Error reporting job result: %v", err)
 		}
 	}
 }
diff --git a/cmd/osbuild-worker/osbuild.go b/cmd/osbuild-worker/osbuild.go
new file mode 100644
index 000000000..7cb032951
--- /dev/null
+++ b/cmd/osbuild-worker/osbuild.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os/exec"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+)
+
+func RunOSBuild(manifest *osbuild.Manifest, store string, errorWriter io.Writer) (*common.ComposeResult, error) {
+	cmd := exec.Command(
+		"osbuild",
+		"--store", store,
+		"--json", "-",
+	)
+	cmd.Stderr = errorWriter
+
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return nil, fmt.Errorf("error setting up stdin for osbuild: %v", err)
+	}
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, fmt.Errorf("error setting up stdout for osbuild: %v", err)
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		return nil, fmt.Errorf("error starting osbuild: %v", err)
+	}
+
+	err = json.NewEncoder(stdin).Encode(manifest)
+	if err != nil {
+		return nil, fmt.Errorf("error encoding osbuild pipeline: %v", err)
+	}
+	// FIXME: handle or comment this possible error
+	_ = stdin.Close()
+
+	var result common.ComposeResult
+	err = json.NewDecoder(stdout).Decode(&result)
+	if err != nil {
+		return nil, fmt.Errorf("error decoding osbuild output: %#v", err)
+	}
+
+	err = cmd.Wait()
+	if err != nil {
+		return &result, err
+	}
+
+	return &result, nil
+}
diff --git a/internal/jobqueue/api.go b/internal/jobqueue/api.go
index bd017c3e4..4196c0f0b 100644
--- a/internal/jobqueue/api.go
+++ b/internal/jobqueue/api.go
@@ -7,10 +7,10 @@ import (
 	"net/http"
 	"strconv"
 
-	"github.com/osbuild/osbuild-composer/internal/store"
-
 	"github.com/google/uuid"
 	"github.com/julienschmidt/httprouter"
+
+	"github.com/osbuild/osbuild-composer/internal/store"
 )
 
 type API struct {
@@ -102,8 +102,8 @@ func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request,
 
 	writer.WriteHeader(http.StatusCreated)
 	// FIXME: handle or comment this possible error
-	_ = json.NewEncoder(writer).Encode(Job{
-		ID:           nextJob.ComposeID,
+	_ = json.NewEncoder(writer).Encode(addJobResponse{
+		ComposeID:    nextJob.ComposeID,
 		ImageBuildID: nextJob.ImageBuildID,
 		Manifest:     nextJob.Manifest,
 		Targets:      nextJob.Targets,
@@ -130,7 +130,7 @@ func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Reque
 		return
 	}
 
-	var body JobStatus
+	var body updateJobRequest
 	err = json.NewDecoder(request.Body).Decode(&body)
 	if err != nil {
 		statusResponseError(writer, http.StatusBadRequest, "invalid status: "+err.Error())
diff --git a/internal/jobqueue/client.go b/internal/jobqueue/client.go
index 6bd49b295..b9f1f4f1f 100644
--- a/internal/jobqueue/client.go
+++ b/internal/jobqueue/client.go
@@ -12,7 +12,11 @@ import (
 	"net"
 	"net/http"
 
+	"github.com/google/uuid"
+
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/target"
 )
 
 type Client struct {
@@ -21,6 +25,13 @@ type Client struct {
 	hostname string
 }
 
+type Job struct {
+	ComposeID    uuid.UUID
+	ImageBuildID int
+	Manifest     *osbuild.Manifest
+	Targets      []*target.Target
+}
+
 func NewClient(address string, conf *tls.Config) *Client {
 	client := &http.Client{
 		Transport: &http.Transport{
@@ -71,22 +82,27 @@ func (c *Client) AddJob() (*Job, error) {
 		return nil, fmt.Errorf("couldn't create job, got %d: %s", response.StatusCode, r)
 	}
 
-	job := &Job{}
-	err = json.NewDecoder(response.Body).Decode(job)
+	var jr addJobResponse
+	err = json.NewDecoder(response.Body).Decode(&jr)
 	if err != nil {
 		return nil, err
 	}
 
-	return job, nil
+	return &Job{
+		jr.ComposeID,
+		jr.ImageBuildID,
+		jr.Manifest,
+		jr.Targets,
+	}, nil
 }
 
 func (c *Client) UpdateJob(job *Job, status common.ImageBuildState, result *common.ComposeResult) error {
 	var b bytes.Buffer
-	err := json.NewEncoder(&b).Encode(&JobStatus{status, result})
+	err := json.NewEncoder(&b).Encode(&updateJobRequest{status, result})
 	if err != nil {
 		panic(err)
 	}
-	urlPath := fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d", job.ID.String(), job.ImageBuildID)
+	urlPath := fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d", job.ComposeID.String(), job.ImageBuildID)
 	url := c.createURL(urlPath)
 	req, err := http.NewRequest("PATCH", url, &b)
 	if err != nil {
@@ -109,7 +125,7 @@ func (c *Client) UpdateJob(job *Job, status common.ImageBuildState, result *comm
 
 func (c *Client) UploadImage(job *Job, reader io.Reader) error {
 	// content type doesn't really matter
-	url := c.createURL(fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d/image", job.ID.String(), job.ImageBuildID))
+	url := c.createURL(fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d/image", job.ComposeID.String(), job.ImageBuildID))
 	_, err := c.client.Post(url, "application/octet-stream", reader)
 
 	return err
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
deleted file mode 100644
index 9de7592ef..000000000
--- a/internal/jobqueue/job.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package jobqueue
-
-import (
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"path"
-
-	"github.com/osbuild/osbuild-composer/internal/osbuild"
-
-	"github.com/google/uuid"
-
-	"github.com/osbuild/osbuild-composer/internal/common"
-	"github.com/osbuild/osbuild-composer/internal/target"
-	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
-)
-
-type Job struct {
-	ID           uuid.UUID         `json:"id"`
-	ImageBuildID int               `json:"image_build_id"`
-	Manifest     *osbuild.Manifest `json:"manifest"`
-	Targets      []*target.Target  `json:"targets"`
-}
-
-type JobStatus struct {
-	Status common.ImageBuildState `json:"status"`
-	Result *common.ComposeResult  `json:"result"`
-}
-
-type TargetsError struct {
-	Errors []error
-}
-
-func (e *TargetsError) Error() string {
-	errString := fmt.Sprintf("%d target(s) errored:\n", len(e.Errors))
-
-	for _, err := range e.Errors {
-		errString += err.Error() + "\n"
-	}
-
-	return errString
-}
-
-func (job *Job) Run(uploader LocalTargetUploader) (*common.ComposeResult, error) {
-	tmpStore, err := ioutil.TempDir("/var/tmp", "osbuild-store")
-	if err != nil {
-		return nil, fmt.Errorf("error setting up osbuild store: %v", err)
-	}
-	// FIXME: how to handle errors in defer?
-	defer os.RemoveAll(tmpStore)
-
-	cmd := exec.Command(
-		"osbuild",
-		"--store", tmpStore,
-		"--json", "-",
-	)
-	cmd.Stderr = os.Stderr
-
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		return nil, fmt.Errorf("error setting up stdin for osbuild: %v", err)
-	}
-
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		return nil, fmt.Errorf("error setting up stdout for osbuild: %v", err)
-	}
-
-	err = cmd.Start()
-	if err != nil {
-		return nil, fmt.Errorf("error starting osbuild: %v", err)
-	}
-
-	err = json.NewEncoder(stdin).Encode(job.Manifest)
-	if err != nil {
-		return nil, fmt.Errorf("error encoding osbuild pipeline: %v", err)
-	}
-	// FIXME: handle or comment this possible error
-	_ = stdin.Close()
-
-	var result common.ComposeResult
-	err = json.NewDecoder(stdout).Decode(&result)
-	if err != nil {
-		return nil, fmt.Errorf("error decoding osbuild output: %#v", err)
-	}
-
-	err = cmd.Wait()
-	if err != nil {
-		return &result, err
-	}
-
-	var r []error
-
-	for _, t := range job.Targets {
-		switch options := t.Options.(type) {
-		case *target.LocalTargetOptions:
-			f, err := os.Open(path.Join(tmpStore, "refs", result.OutputID, options.Filename))
-			if err != nil {
-				r = append(r, err)
-				continue
-			}
-
-			err = uploader.UploadImage(job, f)
-			if err != nil {
-				r = append(r, err)
-				continue
-			}
-
-		case *target.AWSTargetOptions:
-
-			a, err := awsupload.New(options.Region, options.AccessKeyID, options.SecretAccessKey)
-			if err != nil {
-				r = append(r, err)
-				continue
-			}
-
-			if options.Key == "" {
-				options.Key = job.ID.String()
-			}
-
-			_, err = a.Upload(path.Join(tmpStore, "refs", result.OutputID, options.Filename), options.Bucket, options.Key)
-			if err != nil {
-				r = append(r, err)
-				continue
-			}
-
-			/* TODO: communicate back the AMI */
-			_, err = a.Register(t.ImageName, options.Bucket, options.Key)
-			if err != nil {
-				r = append(r, err)
-				continue
-			}
-		case *target.AzureTargetOptions:
-		default:
-			r = append(r, fmt.Errorf("invalid target type"))
-		}
-	}
-
-	if len(r) > 0 {
-		return &result, &TargetsError{r}
-	}
-
-	return &result, nil
-}
diff --git a/internal/jobqueue/json.go b/internal/jobqueue/json.go
new file mode 100644
index 000000000..f324479cd
--- /dev/null
+++ b/internal/jobqueue/json.go
@@ -0,0 +1,21 @@
+package jobqueue
+
+import (
+	"github.com/google/uuid"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/target"
+)
+
+type addJobResponse struct {
+	ComposeID    uuid.UUID         `json:"compose_id"`
+	ImageBuildID int               `json:"image_build_id"`
+	Manifest     *osbuild.Manifest `json:"manifest"`
+	Targets      []*target.Target  `json:"targets"`
+}
+
+type updateJobRequest struct {
+	Status common.ImageBuildState `json:"status"`
+	Result *common.ComposeResult  `json:"result"`
+}
diff --git a/internal/jobqueue/local_target_uploader.go b/internal/jobqueue/local_target_uploader.go
deleted file mode 100644
index 292b34733..000000000
--- a/internal/jobqueue/local_target_uploader.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package jobqueue
-
-import "io"
-
-type LocalTargetUploader interface {
-	UploadImage(job *Job, reader io.Reader) error
-}
diff --git a/internal/store/store.go b/internal/store/store.go
index a073eab59..784cc1ff9 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -57,12 +57,12 @@ type Job struct {
 }
 
 type SourceConfig struct {
-	Name     string `json:"name"`
-	Type     string `json:"type"`
-	URL      string `json:"url"`
-	CheckGPG bool   `json:"check_gpg"`
-	CheckSSL bool   `json:"check_ssl"`
-	System   bool   `json:"system"`
+	Name     string `json:"name" toml:"name"`
+	Type     string `json:"type" toml:"type"`
+	URL      string `json:"url" toml:"url"`
+	CheckGPG bool   `json:"check_gpg" toml:"check_gpg"`
+	CheckSSL bool   `json:"check_ssl" toml:"check_ssl"`
+	System   bool   `json:"system" toml:"system"`
 }
 
 type NotFoundError struct {
diff --git a/internal/upload/awsupload/awsupload.go b/internal/upload/awsupload/awsupload.go
index 255307adf..7936723a3 100644
--- a/internal/upload/awsupload/awsupload.go
+++ b/internal/upload/awsupload/awsupload.go
@@ -9,12 +9,14 @@ import (
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/aws/aws-sdk-go/service/s3/s3manager"
 )
 
 type AWS struct {
 	uploader *s3manager.Uploader
 	importer *ec2.EC2
+	s3       *s3.S3
 }
 
 func New(region, accessKeyID, accessKey string) (*AWS, error) {
@@ -33,6 +35,7 @@ func New(region, accessKeyID, accessKey string) (*AWS, error) {
 	return &AWS{
 		uploader: s3manager.NewUploader(sess),
 		importer: ec2.New(sess),
+		s3:       s3.New(sess),
 	}, nil
 }
 
@@ -125,6 +128,15 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) {
 		return nil, err
 	}
 
+	// we no longer need the object in s3, let's just delete it
+	_, err = a.s3.DeleteObject(&s3.DeleteObjectInput{
+		Bucket: aws.String(bucket),
+		Key:    aws.String(key),
+	})
+	if err != nil {
+		return nil, err
+	}
+
 	importOutput, err := a.importer.DescribeImportSnapshotTasks(
 		&ec2.DescribeImportSnapshotTasksInput{
 			ImportTaskIds: []*string{
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index b4696d89e..93a5e831c 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -379,6 +379,15 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque
 		err = errors_package.New("blueprint must be in json or toml format")
 	}
 
+	// Basic check of the source, should at least have a name and type
+	if err == nil {
+		if len(source.Name) == 0 {
+			err = errors_package.New("'name' field is missing from API v0 request")
+		} else if len(source.Type) == 0 {
+			err = errors_package.New("'type' field is missing from API v0 request")
+		}
+	}
+
 	if err != nil {
 		errors := responseError{
 			ID:  "ProjectsError",
diff --git a/internal/weldr/json.go b/internal/weldr/json.go
index be81cce4c..ed767e243 100644
--- a/internal/weldr/json.go
+++ b/internal/weldr/json.go
@@ -100,14 +100,14 @@ func (s *SourceInfoV0) SourceConfig(sourceName string) (ssc store.SourceConfig,
 
 // SourceConfigV0 holds the source repository information
 type SourceConfigV0 struct {
-	Name     string   `json:"name"`
-	Type     string   `json:"type"`
-	URL      string   `json:"url"`
-	CheckGPG bool     `json:"check_gpg"`
-	CheckSSL bool     `json:"check_ssl"`
-	System   bool     `json:"system"`
-	Proxy    string   `json:"proxy"`
-	GPGUrls  []string `json:"gpgkey_urls"`
+	Name     string   `json:"name" toml:"name"`
+	Type     string   `json:"type" toml:"type"`
+	URL      string   `json:"url" toml:"url"`
+	CheckGPG bool     `json:"check_gpg" toml:"check_gpg"`
+	CheckSSL bool     `json:"check_ssl" toml:"check_ssl"`
+	System   bool     `json:"system" toml:"system"`
+	Proxy    string   `json:"proxy" toml:"proxy"`
+	GPGUrls  []string `json:"gpgkey_urls" toml:"gpgkey_urls"`
 }
 
 // SourceConfig returns a SourceConfig struct populated with the supported variables


Test patch:
diff --git a/cmd/osbuild-image-tests/aws.go b/cmd/osbuild-image-tests/aws.go
new file mode 100644
index 000000000..b000bf70d
--- /dev/null
+++ b/cmd/osbuild-image-tests/aws.go
@@ -0,0 +1,281 @@
+// +build integration
+
+package main
+
+import (
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
+)
+
+type awsCredentials struct {
+	AccessKeyId     string
+	SecretAccessKey string
+	Region          string
+	Bucket          string
+}
+
+// getAWSCredentialsFromEnv gets the credentials from environment variables
+// If none of the environment variables is set, it returns nil.
+// If some but not all environment variables are set, it returns an error.
+func getAWSCredentialsFromEnv() (*awsCredentials, error) {
+	accessKeyId, akExists := os.LookupEnv("AWS_ACCESS_KEY_ID")
+	secretAccessKey, sakExists := os.LookupEnv("AWS_SECRET_ACCESS_KEY")
+	region, regionExists := os.LookupEnv("AWS_REGION")
+	bucket, bucketExists := os.LookupEnv("AWS_BUCKET")
+
+	// Workaround Travis security feature. If non of the variables is set, just ignore the test
+	if !akExists && !sakExists && !bucketExists && !regionExists {
+		return nil, nil
+	}
+	// If only one/two of them are not set, then fail
+	if !akExists || !sakExists || !bucketExists || !regionExists {
+		return nil, errors.New("not all required env variables were set")
+	}
+
+	return &awsCredentials{
+		AccessKeyId:     accessKeyId,
+		SecretAccessKey: secretAccessKey,
+		Region:          region,
+		Bucket:          bucket,
+	}, nil
+}
+
+// encodeBase64 encodes string to base64-encoded string
+func encodeBase64(input string) string {
+	return base64.StdEncoding.EncodeToString([]byte(input))
+}
+
+// createUserData creates cloud-init's user-data that contains user redhat with
+// the specified public key
+func createUserData(publicKeyFile string) (string, error) {
+	publicKey, err := ioutil.ReadFile(publicKeyFile)
+	if err != nil {
+		return "", fmt.Errorf("cannot read the public key: %#v", err)
+	}
+
+	userData := fmt.Sprintf(`#cloud-config
+user: redhat
+ssh_authorized_keys:
+  - %s
+`, string(publicKey))
+
+	return userData, nil
+}
+
+// wrapErrorf returns error constructed using fmt.Errorf from format and any
+// other args. If innerError != nil, it's appended at the end of the new
+// error.
+func wrapErrorf(innerError error, format string, a ...interface{}) error {
+	if innerError != nil {
+		a = append(a, innerError)
+		return fmt.Errorf(format+"\n\ninner error: %#s", a...)
+	}
+
+	return fmt.Errorf(format, a...)
+}
+
+// uploadImageToAWS mimics the upload feature of osbuild-composer.
+// It takes an image and an image name and creates an ec2 instance from them.
+// The s3 key is never returned - the same thing is done in osbuild-composer,
+// the user has no way of getting the s3 key.
+func uploadImageToAWS(c *awsCredentials, imagePath string, imageName string) error {
+	uploader, err := awsupload.New(c.Region, c.AccessKeyId, c.SecretAccessKey)
+	if err != nil {
+		return fmt.Errorf("cannot create aws uploader: %#v", err)
+	}
+
+	_, err = uploader.Upload(imagePath, c.Bucket, imageName)
+	if err != nil {
+		return fmt.Errorf("cannot upload the image: %#v", err)
+	}
+	_, err = uploader.Register(imageName, c.Bucket, imageName)
+	if err != nil {
+		return fmt.Errorf("cannot register the image: %#v", err)
+	}
+
+	return nil
+}
+
+// newEC2 creates EC2 struct from given credentials
+func newEC2(c *awsCredentials) (*ec2.EC2, error) {
+	creds := credentials.NewStaticCredentials(c.AccessKeyId, c.SecretAccessKey, "")
+	sess, err := session.NewSession(&aws.Config{
+		Credentials: creds,
+		Region:      aws.String(c.Region),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("cannot create aws session: %#v", err)
+	}
+
+	return ec2.New(sess), nil
+}
+
+type imageDescription struct {
+	Id         *string
+	SnapshotId *string
+	// this doesn't support multiple snapshots per one image,
+	// because this feature is not supported in composer
+}
+
+// describeEC2Image searches for EC2 image by its name and returns
+// its id and snapshot id
+func describeEC2Image(e *ec2.EC2, imageName string) (*imageDescription, error) {
+	imageDescriptions, err := e.DescribeImages(&ec2.DescribeImagesInput{
+		Filters: []*ec2.Filter{
+			{
+				Name: aws.String("name"),
+				Values: []*string{
+					aws.String(imageName),
+				},
+			},
+		},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("cannot describe the image: %#v", err)
+	}
+	imageId := imageDescriptions.Images[0].ImageId
+	snapshotId := imageDescriptions.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId
+
+	return &imageDescription{
+		Id:         imageId,
+		SnapshotId: snapshotId,
+	}, nil
+}
+
+// deleteEC2Image deletes the specified image and its associated snapshot
+func deleteEC2Image(e *ec2.EC2, imageDesc *imageDescription) error {
+	var retErr error
+
+	// firstly, deregister the image
+	_, err := e.DeregisterImage(&ec2.DeregisterImageInput{
+		ImageId: imageDesc.Id,
+	})
+
+	if err != nil {
+		retErr = wrapErrorf(retErr, "cannot deregister the image: %#v", err)
+	}
+
+	// now it's possible to delete the snapshot
+	_, err = e.DeleteSnapshot(&ec2.DeleteSnapshotInput{
+		SnapshotId: imageDesc.SnapshotId,
+	})
+
+	if err != nil {
+		retErr = wrapErrorf(retErr, "cannot delete the snapshot: %#v", err)
+	}
+
+	return retErr
+}
+
+// withBootedImageInEC2 runs the function f in the context of booted
+// image in AWS EC2
+func withBootedImageInEC2(e *ec2.EC2, imageDesc *imageDescription, publicKey string, f func(address string) error) (retErr error) {
+	// generate user data with given public key
+	userData, err := createUserData(publicKey)
+	if err != nil {
+		return err
+	}
+
+	// Security group must be now generated, because by default
+	// all traffic to EC2 instance is filtered.
+
+	securityGroupName, err := generateRandomString("osbuild-image-tests-security-group-")
+	if err != nil {
+		return fmt.Errorf("cannot generate a random name for the image: %#v", err)
+	}
+
+	// Firstly create a security group
+	securityGroup, err := e.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{
+		GroupName:   aws.String(securityGroupName),
+		Description: aws.String("image-tests-security-group"),
+	})
+	if err != nil {
+		return fmt.Errorf("cannot create a new security group: %#v", err)
+	}
+
+	defer func() {
+		_, err = e.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{
+			GroupId: securityGroup.GroupId,
+		})
+
+		if err != nil {
+			retErr = wrapErrorf(retErr, "cannot delete the security group: %#v", err)
+		}
+	}()
+
+	// Authorize incoming SSH connections.
+	_, err = e.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{
+		CidrIp:     aws.String("0.0.0.0/0"),
+		GroupId:    securityGroup.GroupId,
+		FromPort:   aws.Int64(22),
+		ToPort:     aws.Int64(22),
+		IpProtocol: aws.String("tcp"),
+	})
+	if err != nil {
+		return fmt.Errorf("canot add a rule to the security group: %#v", err)
+	}
+
+	// Finally, run the instance from the given image and with the created security group
+	res, err := e.RunInstances(&ec2.RunInstancesInput{
+		MaxCount:         aws.Int64(1),
+		MinCount:         aws.Int64(1),
+		ImageId:          imageDesc.Id,
+		InstanceType:     aws.String("t3.micro"),
+		SecurityGroupIds: []*string{securityGroup.GroupId},
+		UserData:         aws.String(encodeBase64(userData)),
+	})
+	if err != nil {
+		return fmt.Errorf("cannot create a new instance: %#v", err)
+	}
+
+	describeInstanceInput := &ec2.DescribeInstancesInput{
+		InstanceIds: []*string{
+			res.Instances[0].InstanceId,
+		},
+	}
+
+	defer func() {
+		// We need to terminate the instance now and wait until the termination is done.
+		// Otherwise, it wouldn't be possible to delete the image.
+		_, err = e.TerminateInstances(&ec2.TerminateInstancesInput{
+			InstanceIds: []*string{
+				res.Instances[0].InstanceId,
+			},
+		})
+		if err != nil {
+			retErr = wrapErrorf(retErr, "cannot terminate the instance: %#v", err)
+			return
+		}
+
+		err = e.WaitUntilInstanceTerminated(describeInstanceInput)
+		if err != nil {
+			retErr = wrapErrorf(retErr, "waiting for the instance termination failed: %#v", err)
+		}
+	}()
+
+	// The instance has no IP address yet. It's assigned when the instance
+	// is in the state "EXISTS". However, in this state the instance is not
+	// much usable, therefore wait until "RUNNING" state, in which the instance
+	// actually can do something useful for us.
+	err = e.WaitUntilInstanceRunning(describeInstanceInput)
+	if err != nil {
+		return fmt.Errorf("waiting for the instance to be running failed: %#v", err)
+	}
+
+	// By describing the instance, we can get the ip address.
+	out, err := e.DescribeInstances(describeInstanceInput)
+	if err != nil {
+		return fmt.Errorf("cannot describe the instance: %#v", err)
+	}
+
+	return f(*out.Reservations[0].Instances[0].PublicIpAddress)
+}
diff --git a/cmd/osbuild-image-tests/constants-travis.go b/cmd/osbuild-image-tests/constants-travis.go
index 7028c80ff..f198143db 100644
--- a/cmd/osbuild-image-tests/constants-travis.go
+++ b/cmd/osbuild-image-tests/constants-travis.go
@@ -17,8 +17,16 @@ func getOsbuildCommand(store string) *exec.Cmd {
 	return cmd
 }
 
-var imageInfoPath = "tools/image-info"
-var privateKeyPath = "test/keyring/id_rsa"
-var testCasesDirectoryPath = "test/cases"
-var userDataPath = "test/cloud-init/user-data"
-var metaDataPath = "test/cloud-init/meta-data"
+var testPaths = struct {
+	imageInfo          string
+	privateKey         string
+	testCasesDirectory string
+	userData           string
+	metaData           string
+}{
+	imageInfo:          "tools/image-info",
+	privateKey:         "test/keyring/id_rsa",
+	testCasesDirectory: "test/cases",
+	userData:           "test/cloud-init/user-data",
+	metaData:           "test/cloud-init/meta-data",
+}
diff --git a/cmd/osbuild-image-tests/constants.go b/cmd/osbuild-image-tests/constants.go
index 4236e148c..2a6f17c34 100644
--- a/cmd/osbuild-image-tests/constants.go
+++ b/cmd/osbuild-image-tests/constants.go
@@ -13,8 +13,16 @@ func getOsbuildCommand(store string) *exec.Cmd {
 	)
 }
 
-var imageInfoPath = "/usr/libexec/osbuild-composer/image-info"
-var privateKeyPath = "/usr/share/tests/osbuild-composer/keyring/id_rsa"
-var testCasesDirectoryPath = "/usr/share/tests/osbuild-composer/cases"
-var userDataPath = "/usr/share/tests/osbuild-composer/cloud-init/user-data"
-var metaDataPath = "/usr/share/tests/osbuild-composer/cloud-init/meta-data"
+var testPaths = struct {
+	imageInfo          string
+	privateKey         string
+	testCasesDirectory string
+	userData           string
+	metaData           string
+}{
+	imageInfo:          "/usr/libexec/osbuild-composer/image-info",
+	privateKey:         "/usr/share/tests/osbuild-composer/keyring/id_rsa",
+	testCasesDirectory: "/usr/share/tests/osbuild-composer/cases",
+	userData:           "/usr/share/tests/osbuild-composer/cloud-init/user-data",
+	metaData:           "/usr/share/tests/osbuild-composer/cloud-init/meta-data",
+}
diff --git a/cmd/osbuild-image-tests/context-managers.go b/cmd/osbuild-image-tests/context-managers.go
index 7c66143e9..89974169f 100644
--- a/cmd/osbuild-image-tests/context-managers.go
+++ b/cmd/osbuild-image-tests/context-managers.go
@@ -48,6 +48,22 @@ func withTempFile(dir, pattern string, f func(file *os.File) error) error {
 	return f(tempFile)
 }
 
+func withTempDir(dir, pattern string, f func(dir string) error) error {
+	tempDir, err := ioutil.TempDir(dir, pattern)
+	if err != nil {
+		return fmt.Errorf("cannot create the temporary directory %#v", err)
+	}
+
+	defer func() {
+		err := os.RemoveAll(tempDir)
+		if err != nil {
+			log.Printf("cannot remove the temporary directory: %#v", err)
+		}
+	}()
+
+	return f(tempDir)
+}
+
 // writeCloudInitSO creates cloud-init iso from specified userData and
 // metaData and writes it to the writer
 func writeCloudInitISO(writer io.Writer, userData, metaData string) error {
@@ -78,8 +94,8 @@ func withBootedQemuImage(image string, ns netNS, f func() error) error {
 	return withTempFile("", "osbuild-image-tests-cloudinit", func(cloudInitFile *os.File) error {
 		err := writeCloudInitISO(
 			cloudInitFile,
-			userDataPath,
-			metaDataPath,
+			testPaths.userData,
+			testPaths.metaData,
 		)
 		if err != nil {
 			return err
@@ -173,30 +189,41 @@ func withBootedNspawnDirectory(dir, name string, ns netNS, f func() error) error
 // a path to the result to the function f. The result is deleted
 // immediately after the function returns.
 func withExtractedTarArchive(archive string, f func(dir string) error) error {
-	dir, err := ioutil.TempDir("", "tar-archive")
-	if err != nil {
-		return fmt.Errorf("cannot create a temporary dir: %#v", err)
-	}
+	return withTempDir("", "tar-archive", func(dir string) error {
+		cmd := exec.Command(
+			"tar",
+			"xf", archive,
+			"-C", dir,
+		)
+		cmd.Stderr = os.Stderr
+		cmd.Stdout = os.Stdout
 
-	defer func() {
-		err := os.RemoveAll(dir)
+		err := cmd.Run()
 		if err != nil {
-			log.Printf("cannot remove the temporary dir: %#v", err)
+			return fmt.Errorf("cannot untar the archive: %#v", err)
 		}
-	}()
 
-	cmd := exec.Command(
-		"tar",
-		"xf", archive,
-		"-C", dir,
-	)
-	cmd.Stderr = os.Stderr
-	cmd.Stdout = os.Stdout
+		return f(dir)
+	})
+}
 
-	err = cmd.Run()
-	if err != nil {
-		return fmt.Errorf("cannot untar the archive: %#v", err)
-	}
+// withSSHKeyPair runs the function f with a newly generated
+// ssh key-pair, they key-pair is deleted immediately after
+// the function f returns
+func withSSHKeyPair(f func(privateKey, publicKey string) error) error {
+	return withTempDir("", "keys", func(dir string) error {
+		privateKey := dir + "/id_rsa"
+		publicKey := dir + "/id_rsa.pub"
+		cmd := exec.Command("ssh-keygen",
+			"-N", "",
+			"-f", privateKey,
+		)
+
+		err := cmd.Run()
+		if err != nil {
+			return fmt.Errorf("ssh-keygen failed: %#v", err)
+		}
 
-	return f(dir)
+		return f(privateKey, publicKey)
+	})
 }
diff --git a/cmd/osbuild-image-tests/helpers.go b/cmd/osbuild-image-tests/helpers.go
index 75f6da2b9..d67954913 100644
--- a/cmd/osbuild-image-tests/helpers.go
+++ b/cmd/osbuild-image-tests/helpers.go
@@ -7,6 +7,8 @@ import (
 	"os"
 	"syscall"
 	"time"
+
+	"github.com/google/uuid"
 )
 
 // durationMin returns the smaller of two given durations
@@ -50,3 +52,14 @@ func killProcessCleanly(process *os.Process, timeout time.Duration) error {
 
 	return process.Kill()
 }
+
+// generateRandomString generates a new random string with specified prefix.
+// The random part is based on UUID.
+func generateRandomString(prefix string) (string, error) {
+	id, err := uuid.NewRandom()
+	if err != nil {
+		return "", err
+	}
+
+	return prefix + id.String(), nil
+}
diff --git a/cmd/osbuild-image-tests/main_test.go b/cmd/osbuild-image-tests/main_test.go
index c92c76602..6652fea41 100644
--- a/cmd/osbuild-image-tests/main_test.go
+++ b/cmd/osbuild-image-tests/main_test.go
@@ -50,7 +50,9 @@ func runOsbuild(manifest []byte, store string) (string, error) {
 
 	if err != nil {
 		if _, ok := err.(*exec.ExitError); ok {
-			return "", fmt.Errorf("running osbuild failed: %s", outBuffer.String())
+			var formattedOutput bytes.Buffer
+			_ = json.Indent(&formattedOutput, outBuffer.Bytes(), "", "  ")
+			return "", fmt.Errorf("running osbuild failed: %s", formattedOutput.String())
 		}
 		return "", fmt.Errorf("running osbuild failed from an unexpected reason: %#v", err)
 	}
@@ -94,22 +96,22 @@ func splitExtension(path string) (string, string) {
 func testImageInfo(t *testing.T, imagePath string, rawImageInfoExpected []byte) {
 	var imageInfoExpected interface{}
 	err := json.Unmarshal(rawImageInfoExpected, &imageInfoExpected)
-	require.Nilf(t, err, "cannot decode expected image info: %#v", err)
+	require.NoErrorf(t, err, "cannot decode expected image info: %#v", err)
 
-	cmd := exec.Command(imageInfoPath, imagePath)
+	cmd := exec.Command(testPaths.imageInfo, imagePath)
 	cmd.Stderr = os.Stderr
 	reader, writer := io.Pipe()
 	cmd.Stdout = writer
 
 	err = cmd.Start()
-	require.Nilf(t, err, "image-info cannot start: %#v", err)
+	require.NoErrorf(t, err, "image-info cannot start: %#v", err)
 
 	var imageInfoGot interface{}
 	err = json.NewDecoder(reader).Decode(&imageInfoGot)
-	require.Nilf(t, err, "decoding image-info output failed: %#v", err)
+	require.NoErrorf(t, err, "decoding image-info output failed: %#v", err)
 
 	err = cmd.Wait()
-	require.Nilf(t, err, "running image-info failed: %#v", err)
+	require.NoErrorf(t, err, "running image-info failed: %#v", err)
 
 	assert.Equal(t, imageInfoExpected, imageInfoGot)
 }
@@ -123,19 +125,28 @@ func (*timeoutError) Error() string { return "" }
 // that 10 seconds or if systemd-is-running returns starting.
 // It returns nil if systemd-is-running returns running or degraded.
 // It can also return other errors in other error cases.
-func trySSHOnce(ns netNS) error {
+func trySSHOnce(address string, privateKey string, ns *netNS) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
-	cmd := ns.NamespacedCommandContext(
-		ctx,
-		"ssh",
+
+	cmdName := "ssh"
+	cmdArgs := []string{
 		"-p", "22",
-		"-i", privateKeyPath,
+		"-i", privateKey,
 		"-o", "StrictHostKeyChecking=no",
 		"-o", "UserKnownHostsFile=/dev/null",
-		"redhat@localhost",
+		"redhat@" + address,
 		"systemctl --wait is-system-running",
-	)
+	}
+
+	var cmd *exec.Cmd
+
+	if ns != nil {
+		cmd = ns.NamespacedCommandContext(ctx, cmdName, cmdArgs...)
+	} else {
+		cmd = exec.CommandContext(ctx, cmdName, cmdArgs...)
+	}
+
 	output, err := cmd.Output()
 
 	if ctx.Err() == context.DeadlineExceeded {
@@ -169,10 +180,10 @@ func trySSHOnce(ns netNS) error {
 // testSSH tests the running image using ssh.
 // It tries 20 attempts before giving up. If a major error occurs, it might
 // return earlier.
-func testSSH(t *testing.T, ns netNS) {
+func testSSH(t *testing.T, address string, privateKey string, ns *netNS) {
 	const attempts = 20
 	for i := 0; i < attempts; i++ {
-		err := trySSHOnce(ns)
+		err := trySSHOnce(address, privateKey, ns)
 		if err == nil {
 			// pass the test
 			return
@@ -189,39 +200,103 @@ func testSSH(t *testing.T, ns netNS) {
 	t.Errorf("ssh test failure, %d attempts were made", attempts)
 }
 
+func testBootUsingQemu(t *testing.T, imagePath string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withBootedQemuImage(imagePath, ns, func() error {
+			testSSH(t, "localhost", testPaths.privateKey, &ns)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingNspawnImage(t *testing.T, imagePath string, outputID string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withBootedNspawnImage(imagePath, outputID, ns, func() error {
+			testSSH(t, "localhost", testPaths.privateKey, &ns)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingNspawnDirectory(t *testing.T, imagePath string, outputID string) {
+	err := withNetworkNamespace(func(ns netNS) error {
+		return withExtractedTarArchive(imagePath, func(dir string) error {
+			return withBootedNspawnDirectory(dir, outputID, ns, func() error {
+				testSSH(t, "localhost", testPaths.privateKey, &ns)
+				return nil
+			})
+		})
+	})
+	require.NoError(t, err)
+}
+
+func testBootUsingAWS(t *testing.T, imagePath string) {
+	creds, err := getAWSCredentialsFromEnv()
+	require.NoError(t, err)
+
+	// if no credentials are given, fall back to qemu
+	if creds == nil {
+		log.Print("no AWS credentials given, falling back to booting using qemu")
+		testBootUsingQemu(t, imagePath)
+		return
+
+	}
+
+	imageName, err := generateRandomString("osbuild-image-tests-image-")
+	require.NoError(t, err)
+
+	e, err := newEC2(creds)
+	require.NoError(t, err)
+
+	// the following line should be done by osbuild-composer at some point
+	err = uploadImageToAWS(creds, imagePath, imageName)
+	require.NoErrorf(t, err, "upload to amazon failed, resources could have been leaked")
+
+	imageDesc, err := describeEC2Image(e, imageName)
+	require.NoErrorf(t, err, "cannot describe the ec2 image")
+
+	// delete the image after the test is over
+	defer func() {
+		err = deleteEC2Image(e, imageDesc)
+		require.NoErrorf(t, err, "cannot delete the ec2 image, resources could have been leaked")
+	}()
+
+	// boot the uploaded image and try to connect to it
+	err = withSSHKeyPair(func(privateKey, publicKey string) error {
+		return withBootedImageInEC2(e, imageDesc, publicKey, func(address string) error {
+			testSSH(t, address, privateKey, nil)
+			return nil
+		})
+	})
+	require.NoError(t, err)
+}
+
 // testBoot tests if the image is able to successfully boot
 // Before the test it boots the image respecting the specified bootType.
 // The test passes if the function is able to connect to the image via ssh
 // in defined number of attempts and systemd-is-running returns running
 // or degraded status.
 func testBoot(t *testing.T, imagePath string, bootType string, outputID string) {
-	err := withNetworkNamespace(func(ns netNS) error {
-		switch bootType {
-		case "qemu":
-			fallthrough
-		case "qemu-extract":
-			return withBootedQemuImage(imagePath, ns, func() error {
-				testSSH(t, ns)
-				return nil
-			})
-		case "nspawn":
-			return withBootedNspawnImage(imagePath, outputID, ns, func() error {
-				testSSH(t, ns)
-				return nil
-			})
-		case "nspawn-extract":
-			return withExtractedTarArchive(imagePath, func(dir string) error {
-				return withBootedNspawnDirectory(dir, outputID, ns, func() error {
-					testSSH(t, ns)
-					return nil
-				})
-			})
-		default:
-			panic("unknown boot type!")
-		}
-	})
+	switch bootType {
+	case "qemu":
+		fallthrough
+	case "qemu-extract":
+		testBootUsingQemu(t, imagePath)
+
+	case "nspawn":
+		testBootUsingNspawnImage(t, imagePath, outputID)
+
+	case "nspawn-extract":
+		testBootUsingNspawnDirectory(t, imagePath, outputID)
 
-	require.Nil(t, err)
+	case "aws":
+		testBootUsingAWS(t, imagePath)
+
+	default:
+		panic("unknown boot type!")
+	}
 }
 
 // testImage performs a series of tests specified in the testcase
@@ -244,7 +319,7 @@ func testImage(t *testing.T, testcase testcaseStruct, imagePath, outputID string
 // tests the result
 func runTestcase(t *testing.T, testcase testcaseStruct) {
 	store, err := ioutil.TempDir("/var/tmp", "osbuild-image-tests-")
-	require.Nilf(t, err, "cannot create temporary store: %#v", err)
+	require.NoErrorf(t, err, "cannot create temporary store: %#v", err)
 
 	defer func() {
 		err := os.RemoveAll(store)
@@ -254,7 +329,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 	}()
 
 	outputID, err := runOsbuild(testcase.Manifest, store)
-	require.Nil(t, err)
+	require.NoError(t, err)
 
 	imagePath := fmt.Sprintf("%s/refs/%s/%s", store, outputID, testcase.ComposeRequest.Filename)
 
@@ -264,7 +339,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 		_, ex = splitExtension(base)
 		if ex != ".tar" {
 			err := extractXZ(imagePath)
-			require.Nil(t, err)
+			require.NoError(t, err)
 			imagePath = base
 		}
 	}
@@ -274,7 +349,7 @@ func runTestcase(t *testing.T, testcase testcaseStruct) {
 
 // getAllCases returns paths to all testcases in the testcase directory
 func getAllCases() ([]string, error) {
-	cases, err := ioutil.ReadDir(testCasesDirectoryPath)
+	cases, err := ioutil.ReadDir(testPaths.testCasesDirectory)
 	if err != nil {
 		return nil, fmt.Errorf("cannot list test cases: %#v", err)
 	}
@@ -285,7 +360,7 @@ func getAllCases() ([]string, error) {
 			continue
 		}
 
-		casePath := fmt.Sprintf("%s/%s", testCasesDirectoryPath, c.Name())
+		casePath := fmt.Sprintf("%s/%s", testPaths.testCasesDirectory, c.Name())
 		casesPaths = append(casesPaths, casePath)
 	}
 
@@ -297,11 +372,11 @@ func runTests(t *testing.T, cases []string) {
 	for _, path := range cases {
 		t.Run(path, func(t *testing.T) {
 			f, err := os.Open(path)
-			require.Nilf(t, err, "%s: cannot open test case: %#v", path, err)
+			require.NoErrorf(t, err, "%s: cannot open test case: %#v", path, err)
 
 			var testcase testcaseStruct
 			err = json.NewDecoder(f).Decode(&testcase)
-			require.Nilf(t, err, "%s: cannot decode test case: %#v", path, err)
+			require.NoErrorf(t, err, "%s: cannot decode test case: %#v", path, err)
 
 			currentArch := common.CurrentArch()
 			if testcase.ComposeRequest.Arch != currentArch {
@@ -320,7 +395,7 @@ func TestImages(t *testing.T) {
 	if len(cases) == 0 {
 		var err error
 		cases, err = getAllCases()
-		require.Nil(t, err)
+		require.NoError(t, err)
 	}
 
 	runTests(t, cases)
diff --git a/internal/client/source_test.go b/internal/client/source_test.go
index e8a5eedcd..49dd45158 100644
--- a/internal/client/source_test.go
+++ b/internal/client/source_test.go
@@ -109,6 +109,25 @@ func TestPOSTInvalidTOMLSourceV0(t *testing.T) {
 	require.False(t, resp.Status, "did not return an error")
 }
 
+// POST a wrong TOML source
+func TestPOSTWrongTOMLSourceV0(t *testing.T) {
+	// Should not have a [] section
+	source := `
+		[package-repo-toml-v0]
+		name = "package-repo-toml-v0"
+		url = "file://REPO-PATH
+		type = "yum-baseurl"
+		proxy = "https://proxy-url/"
+		check_ssl = true
+		check_gpg = true
+		gpgkey_urls = ["https://url/path/to/gpg-key"]
+	`
+
+	resp, err := PostTOMLSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
 // list sources
 func TestListSourcesV0(t *testing.T) {
 	sources := []string{`{
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 382dccbe0..c1a28d413 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -4,13 +4,13 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/google/uuid"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/test"
-
-	"github.com/google/uuid"
 )
 
 func TestBasic(t *testing.T) {
@@ -57,7 +57,7 @@ func TestCreate(t *testing.T) {
 	}
 
 	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"`+id.String()+`","image_build_id":0,"manifest":{"sources":{},"pipeline":{}},"targets":[]}`, "created")
+		`{"compose_id":"`+id.String()+`","image_build_id":0,"manifest":{"sources":{},"pipeline":{}},"targets":[]}`, "created")
 }
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index c5c29efbc..2f7f680c4 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -878,6 +878,31 @@ check_gpg = false
 	test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 }
 
+// Empty TOML, and invalid TOML should return an error
+func TestSourcesNewWrongToml(t *testing.T) {
+	sources := []string{``, `
+[fish]
+name = "fish"
+url = "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/"
+type = "yum-baseurl"
+check_ssl = false
+check_gpg = false
+`}
+	for _, source := range sources {
+		req := httptest.NewRequest("POST", "/api/v0/projects/source/new", bytes.NewReader([]byte(source)))
+		req.Header.Set("Content-Type", "text/x-toml")
+		recorder := httptest.NewRecorder()
+
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+		api.ServeHTTP(recorder, req)
+
+		r := recorder.Result()
+		if r.StatusCode != http.StatusBadRequest {
+			t.Errorf("unexpected status %v", r.StatusCode)
+		}
+	}
+}
+
 func TestSourcesInfo(t *testing.T) {
 	sourceStr := `{"name":"fish","type":"yum-baseurl","url":"https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","check_gpg":false,"check_ssl":false,"system":false}`
 
diff --git a/jenkins/run_tests.sh b/jenkins/run_tests.sh
index f02a293f2..042e299ba 100644
--- a/jenkins/run_tests.sh
+++ b/jenkins/run_tests.sh
@@ -21,8 +21,8 @@ pushd ansible-osbuild
 popd
 
 # Run the integration tests.
-# /usr/libexec/tests/osbuild-composer/osbuild-dnf-json-tests
+/usr/libexec/tests/osbuild-composer/osbuild-dnf-json-tests -test.v
 # /usr/libexec/tests/osbuild-composer/osbuild-image-tests
-# /usr/libexec/tests/osbuild-composer/osbuild-rcm-tests -test.v
+/usr/libexec/tests/osbuild-composer/osbuild-rcm-tests -test.v
 # /usr/libexec/tests/osbuild-composer/osbuild-tests -test.v
 # /usr/libexec/tests/osbuild-composer/osbuild-weldr-tests -test.v
diff --git a/test/cases/f30-x86_64-ami-boot.json b/test/cases/f30-x86_64-ami-boot.json
index ba125b2b6..19da682b2 100644
--- a/test/cases/f30-x86_64-ami-boot.json
+++ b/test/cases/f30-x86_64-ami-boot.json
@@ -1,6 +1,6 @@
 {
   "boot": {
-    "type": "qemu-extract"
+    "type": "aws"
   },
   "compose-request": {
     "distro": "fedora-30",
diff --git a/test/cases/f31-x86_64-ami-boot.json b/test/cases/f31-x86_64-ami-boot.json
index 18e0551b9..c26203d51 100644
--- a/test/cases/f31-x86_64-ami-boot.json
+++ b/test/cases/f31-x86_64-ami-boot.json
@@ -1,6 +1,6 @@
 {
   "boot": {
-    "type": "qemu-extract"
+    "type": "aws"
   },
   "compose-request": {
     "distro": "fedora-31",
diff --git a/tools/test-case-generators/format-request-map.json b/tools/test-case-generators/format-request-map.json
index a62993d88..712f0ed89 100644
--- a/tools/test-case-generators/format-request-map.json
+++ b/tools/test-case-generators/format-request-map.json
@@ -1,7 +1,7 @@
 {
   "ami": {
     "boot": {
-      "type": "qemu-extract"
+      "type": "aws"
     },
     "compose-request": {
       "distro": "",