cloudapi: Add endpoints to clone aws images across regions

Support for creating multiple amis from a single compose. It uses the
AWSEC2* jobs to push images to new regions, and share them with new
accounts.

The compose it depends upon has to have succeeded.
This commit is contained in:
Sanne Raymaekers 2022-06-30 11:46:37 +02:00 committed by Ondřej Budai
parent 599829a3b8
commit d13347e1ca
8 changed files with 794 additions and 117 deletions

View file

@ -44,6 +44,10 @@ const (
ErrorTenantNotFound ServiceErrorCode = 28
ErrorNoGPGKey ServiceErrorCode = 29
ErrorValidationFailed ServiceErrorCode = 30
ErrorComposeBadState ServiceErrorCode = 31
ErrorUnsupportedImage ServiceErrorCode = 32
ErrorInvalidImageFromComposeId ServiceErrorCode = 33
ErrorImageNotFound ServiceErrorCode = 34
// Internal errors, these are bugs
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
@ -63,6 +67,9 @@ const (
ErrorDepsolveJobCanceled ServiceErrorCode = 1014
ErrorUnexpectedNumberOfImageBuilds ServiceErrorCode = 1015
ErrorGettingBuildDependencyStatus ServiceErrorCode = 1016
ErrorGettingOSBuildJobStatus ServiceErrorCode = 1017
ErrorGettingAWSEC2JobStatus ServiceErrorCode = 1018
ErrorGettingJobType ServiceErrorCode = 1019
// Errors contained within this file
ErrorUnspecified ServiceErrorCode = 10000
@ -106,12 +113,16 @@ func getServiceErrors() serviceErrors {
serviceError{ErrorMethodNotAllowed, http.StatusMethodNotAllowed, "Requested method isn't supported for resource"},
serviceError{ErrorNotAcceptable, http.StatusNotAcceptable, "Only 'application/json' content is supported"},
serviceError{ErrorNoBaseURLInPayloadRepository, http.StatusBadRequest, "BaseURL must be specified for payload repositories"},
serviceError{ErrorInvalidJobType, http.StatusNotFound, "Requested job has invalid type"},
serviceError{ErrorInvalidJobType, http.StatusNotFound, "Job with given id has an invalid type"},
serviceError{ErrorInvalidNumberOfImageBuilds, http.StatusBadRequest, "Compose request has unsupported number of image builds"},
serviceError{ErrorInvalidOSTreeParams, http.StatusBadRequest, "Invalid OSTree parameters or parameter combination"},
serviceError{ErrorTenantNotFound, http.StatusBadRequest, "Tenant not found in JWT claims"},
serviceError{ErrorNoGPGKey, http.StatusBadRequest, "Invalid repository, when check_gpg is set, gpgkey must be specified"},
serviceError{ErrorValidationFailed, http.StatusBadRequest, "Request could not be validated"},
serviceError{ErrorComposeBadState, http.StatusBadRequest, "Compose is running or has failed"},
serviceError{ErrorUnsupportedImage, http.StatusBadRequest, "This compose doesn't support the creation of multiple images"},
serviceError{ErrorInvalidImageFromComposeId, http.StatusBadRequest, "Invalid format for image id"},
serviceError{ErrorImageNotFound, http.StatusBadRequest, "Image with given id not found"},
serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
@ -130,6 +141,9 @@ func getServiceErrors() serviceErrors {
serviceError{ErrorDepsolveJobCanceled, http.StatusInternalServerError, "Depsolve job was cancelled"},
serviceError{ErrorUnexpectedNumberOfImageBuilds, http.StatusInternalServerError, "Compose has unexpected number of image builds"},
serviceError{ErrorGettingBuildDependencyStatus, http.StatusInternalServerError, "Error checking status of build job dependencies"},
serviceError{ErrorGettingOSBuildJobStatus, http.StatusInternalServerError, "Unable to get osbuild job status"},
serviceError{ErrorGettingAWSEC2JobStatus, http.StatusInternalServerError, "Unable to get ec2 job status"},
serviceError{ErrorGettingJobType, http.StatusInternalServerError, "Unable to get job type of existing job"},
serviceError{ErrorUnspecified, http.StatusInternalServerError, "Unspecified internal error "},
serviceError{ErrorNotHTTPError, http.StatusInternalServerError, "Error is not an instance of HTTPError"},

View file

@ -648,8 +648,7 @@ func (h *apiHandlers) getComposeStatusImpl(ctx echo.Context, id string) error {
if err != nil {
return HTTPError(ErrorUnknownUploadTarget)
}
// TODO: determine upload status based on the target results, not job results
us.Status = UploadStatusValue(result.UploadStatus)
us.Status = uploadStatusFromJobStatus(jobInfo.JobStatus, result.JobError)
}
return ctx.JSON(http.StatusOK, ComposeStatus{
@ -705,8 +704,7 @@ func (h *apiHandlers) getComposeStatusImpl(ctx echo.Context, id string) error {
if err != nil {
return HTTPError(ErrorUnknownUploadTarget)
}
// TODO: determine upload status based on the target results, not job results
us.Status = UploadStatusValue(buildJobResult.UploadStatus)
us.Status = uploadStatusFromJobStatus(buildInfo.JobStatus, result.JobError)
}
}
@ -1234,3 +1232,227 @@ func genRepoConfig(repo Repository) (*rpmmd.RepoConfig, error) {
return repoConfig, nil
}
func (h *apiHandlers) PostCloneCompose(ctx echo.Context, id string) error {
return h.server.EnsureJobChannel(h.postCloneComposeImpl)(ctx, id)
}
func (h *apiHandlers) postCloneComposeImpl(ctx echo.Context, id string) error {
channel, err := h.server.getTenantChannel(ctx)
if err != nil {
return HTTPErrorWithInternal(ErrorTenantNotFound, err)
}
jobId, err := uuid.Parse(id)
if err != nil {
return HTTPError(ErrorInvalidComposeId)
}
jobType, err := h.server.workers.JobType(jobId)
if err != nil {
return HTTPError(ErrorComposeNotFound)
}
if jobType != worker.JobTypeOSBuild {
return HTTPError(ErrorInvalidJobType)
}
var osbuildResult worker.OSBuildJobResult
osbuildInfo, err := h.server.workers.OSBuildJobInfo(jobId, &osbuildResult)
if err != nil {
return HTTPErrorWithInternal(ErrorGettingOSBuildJobStatus, err)
}
if osbuildInfo.JobStatus.Finished.IsZero() || !osbuildResult.Success {
return HTTPError(ErrorComposeBadState)
}
if osbuildResult.TargetResults == nil {
return HTTPError(ErrorMalformedOSBuildJobResult)
}
// Only single upload target is allowed, therefore only a single upload target result is allowed as well
if len(osbuildResult.TargetResults) != 1 {
return HTTPError(ErrorSeveralUploadTargets)
}
var us *UploadStatus
us, err = targetResultToUploadStatus(osbuildResult.TargetResults[0])
if err != nil {
return HTTPError(ErrorUnknownUploadTarget)
}
var osbuildJob worker.OSBuildJob
err = h.server.workers.OSBuildJob(jobId, &osbuildJob)
if err != nil {
return HTTPErrorWithInternal(ErrorComposeNotFound, err)
}
if len(osbuildJob.Targets) != 1 {
return HTTPError(ErrorSeveralUploadTargets)
}
// the id of the last job in the dependency chain which users should wait on
finalJob := jobId
// look at the upload status of the osbuild dependency to decide what to do
if us.Type == UploadTypesAws {
options := us.Options.(AWSEC2UploadStatus)
var img AWSEC2CloneCompose
err := ctx.Bind(&img)
if err != nil {
return err
}
shareAmi := options.Ami
shareRegion := img.Region
if img.Region != options.Region {
// Let the share job use dynArgs
shareAmi = ""
shareRegion = ""
// Check dependents if we need to do a copyjob
foundDep := false
for _, d := range osbuildInfo.Dependents {
jt, err := h.server.workers.JobType(d)
if err != nil {
return HTTPErrorWithInternal(ErrorGettingJobType, err)
}
if jt == worker.JobTypeAWSEC2Copy {
var cjResult worker.AWSEC2CopyJobResult
_, err := h.server.workers.AWSEC2CopyJobInfo(d, &cjResult)
if err != nil {
return HTTPErrorWithInternal(ErrorGettingAWSEC2JobStatus, err)
}
if cjResult.JobError == nil && options.Region == cjResult.Region {
finalJob = d
foundDep = true
break
}
}
}
if !foundDep {
copyJob := &worker.AWSEC2CopyJob{
Ami: options.Ami,
SourceRegion: options.Region,
TargetRegion: img.Region,
TargetName: fmt.Sprintf("composer-api-%s", uuid.New().String()),
}
finalJob, err = h.server.workers.EnqueueAWSEC2CopyJob(copyJob, finalJob, channel)
if err != nil {
return HTTPErrorWithInternal(ErrorEnqueueingJob, err)
}
}
}
var shares []string
awsT, ok := (osbuildJob.Targets[0].Options).(*target.AWSTargetOptions)
if !ok {
return HTTPError(ErrorUnknownUploadTarget)
}
if len(awsT.ShareWithAccounts) > 0 {
shares = append(shares, awsT.ShareWithAccounts...)
}
if img.ShareWithAccounts != nil && len(*img.ShareWithAccounts) > 0 {
shares = append(shares, (*img.ShareWithAccounts)...)
}
if len(shares) > 0 {
shareJob := &worker.AWSEC2ShareJob{
Ami: shareAmi,
Region: shareRegion,
ShareWithAccounts: shares,
}
finalJob, err = h.server.workers.EnqueueAWSEC2ShareJob(shareJob, finalJob, channel)
if err != nil {
return HTTPErrorWithInternal(ErrorEnqueueingJob, err)
}
}
} else {
return HTTPError(ErrorUnsupportedImage)
}
return ctx.JSON(http.StatusCreated, CloneComposeResponse{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/image-builder-composer/v2/composes/%v/clone", jobId),
Id: finalJob.String(),
Kind: "CloneComposeId",
},
Id: finalJob.String(),
})
}
func (h *apiHandlers) GetCloneStatus(ctx echo.Context, id string) error {
return h.server.EnsureJobChannel(h.getCloneStatus)(ctx, id)
}
func (h *apiHandlers) getCloneStatus(ctx echo.Context, id string) error {
jobId, err := uuid.Parse(id)
if err != nil {
return HTTPError(ErrorInvalidComposeId)
}
jobType, err := h.server.workers.JobType(jobId)
if err != nil {
return HTTPError(ErrorComposeNotFound)
}
var us UploadStatus
switch jobType {
case worker.JobTypeAWSEC2Copy:
var result worker.AWSEC2CopyJobResult
info, err := h.server.workers.AWSEC2CopyJobInfo(jobId, &result)
if err != nil {
return HTTPError(ErrorGettingAWSEC2JobStatus)
}
us = UploadStatus{
Status: uploadStatusFromJobStatus(info.JobStatus, result.JobError),
Type: UploadTypesAws,
Options: AWSEC2UploadStatus{
Ami: result.Ami,
Region: result.Region,
},
}
case worker.JobTypeAWSEC2Share:
var result worker.AWSEC2ShareJobResult
info, err := h.server.workers.AWSEC2ShareJobInfo(jobId, &result)
if err != nil {
return HTTPError(ErrorGettingAWSEC2JobStatus)
}
us = UploadStatus{
Status: uploadStatusFromJobStatus(info.JobStatus, result.JobError),
Type: UploadTypesAws,
Options: AWSEC2UploadStatus{
Ami: result.Ami,
Region: result.Region,
},
}
default:
return HTTPError(ErrorInvalidJobType)
}
return ctx.JSON(http.StatusOK, CloneStatus{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", jobId),
Id: jobId.String(),
Kind: "CloneComposeStatus",
},
UploadStatus: us,
})
}
// TODO: determine upload status based on the target results, not job results
func uploadStatusFromJobStatus(js *worker.JobStatus, je *clienterrors.Error) UploadStatusValue {
if je != nil || js.Canceled {
return UploadStatusValueFailure
}
if js.Started.IsZero() {
return UploadStatusValuePending
}
if js.Finished.IsZero() {
return UploadStatusValueRunning
}
return UploadStatusValueSuccess
}

View file

@ -101,6 +101,12 @@ const (
UploadTypesGcp UploadTypes = "gcp"
)
// AWSEC2CloneCompose defines model for AWSEC2CloneCompose.
type AWSEC2CloneCompose struct {
Region string `json:"region"`
ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"`
}
// AWSEC2UploadOptions defines model for AWSEC2UploadOptions.
type AWSEC2UploadOptions struct {
Region string `json:"region"`
@ -153,6 +159,25 @@ type AzureUploadStatus struct {
ImageName string `json:"image_name"`
}
// CloneComposeBody defines model for CloneComposeBody.
type CloneComposeBody interface{}
// CloneComposeResponse defines model for CloneComposeResponse.
type CloneComposeResponse struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference `yaml:",inline"`
// Embedded fields due to inline allOf schema
Id string `json:"id"`
}
// CloneStatus defines model for CloneStatus.
type CloneStatus struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference `yaml:",inline"`
// Embedded struct due to allOf(#/components/schemas/UploadStatus)
UploadStatus `yaml:",inline"`
}
// ComposeId defines model for ComposeId.
type ComposeId struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
@ -481,6 +506,9 @@ type Size string
// PostComposeJSONBody defines parameters for PostCompose.
type PostComposeJSONBody ComposeRequest
// PostCloneComposeJSONBody defines parameters for PostCloneCompose.
type PostCloneComposeJSONBody CloneComposeBody
// GetErrorListParams defines parameters for GetErrorList.
type GetErrorListParams struct {
// Page index
@ -493,14 +521,23 @@ type GetErrorListParams struct {
// PostComposeJSONRequestBody defines body for PostCompose for application/json ContentType.
type PostComposeJSONRequestBody PostComposeJSONBody
// PostCloneComposeJSONRequestBody defines body for PostCloneCompose for application/json ContentType.
type PostCloneComposeJSONRequestBody PostCloneComposeJSONBody
// ServerInterface represents all server handlers.
type ServerInterface interface {
// The status of a cloned compose
// (GET /clones/{id})
GetCloneStatus(ctx echo.Context, id string) error
// Create compose
// (POST /compose)
PostCompose(ctx echo.Context) error
// The status of a compose
// (GET /composes/{id})
GetComposeStatus(ctx echo.Context, id string) error
// Clone an existing compose
// (POST /composes/{id}/clone)
PostCloneCompose(ctx echo.Context, id string) error
// Get logs for a compose.
// (GET /composes/{id}/logs)
GetComposeLogs(ctx echo.Context, id string) error
@ -526,6 +563,24 @@ type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// GetCloneStatus converts echo context to params.
func (w *ServerInterfaceWrapper) GetCloneStatus(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id string
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
ctx.Set(BearerScopes, []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetCloneStatus(ctx, id)
return err
}
// PostCompose converts echo context to params.
func (w *ServerInterfaceWrapper) PostCompose(ctx echo.Context) error {
var err error
@ -555,6 +610,22 @@ func (w *ServerInterfaceWrapper) GetComposeStatus(ctx echo.Context) error {
return err
}
// PostCloneCompose converts echo context to params.
func (w *ServerInterfaceWrapper) PostCloneCompose(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id string
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.PostCloneCompose(ctx, id)
return err
}
// GetComposeLogs converts echo context to params.
func (w *ServerInterfaceWrapper) GetComposeLogs(ctx echo.Context) error {
var err error
@ -689,8 +760,10 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
Handler: si,
}
router.GET(baseURL+"/clones/:id", wrapper.GetCloneStatus)
router.POST(baseURL+"/compose", wrapper.PostCompose)
router.GET(baseURL+"/composes/:id", wrapper.GetComposeStatus)
router.POST(baseURL+"/composes/:id/clone", wrapper.PostCloneCompose)
router.GET(baseURL+"/composes/:id/logs", wrapper.GetComposeLogs)
router.GET(baseURL+"/composes/:id/manifests", wrapper.GetComposeManifests)
router.GET(baseURL+"/composes/:id/metadata", wrapper.GetComposeMetadata)
@ -703,114 +776,116 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"R6B2l8FfAAA=",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file

View file

@ -211,6 +211,101 @@ paths:
schema:
type: string
/composes/{id}/clone:
post:
operationId: postCloneCompose
summary: Clone an existing compose
parameters:
- in: path
name: id
schema:
type: string
format: uuid
example: 123e4567-e89b-12d3-a456-426655440000
required: true
description: ID of the compose
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CloneComposeBody'
responses:
'201':
description: The new image is being created
content:
application/json:
schema:
$ref: '#/components/schemas/CloneComposeResponse'
'400':
description: Invalid compose id
content:
text/plain:
schema:
type: string
'404':
description: Unknown compose id
content:
text/plain:
schema:
type: string
/clones/{id}:
get:
operationId: getCloneStatus
summary: The status of a cloned compose
security:
- Bearer: []
parameters:
- in: path
name: id
schema:
type: string
format: uuid
example: '123e4567-e89b-12d3-a456-426655440000'
required: true
description: ID of image status to get
description: |-
Get the status of a running or completed image from a compose.
This includes whether or not the image creation succeeded.
responses:
'200':
description: image status
content:
application/json:
schema:
$ref: '#/components/schemas/CloneStatus'
'400':
description: Invalid compose id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Auth token is invalid
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Unauthorized to perform operation
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Unknown compose id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Unexpected error occurred
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/compose:
post:
operationId: postCompose
@ -1030,6 +1125,40 @@ components:
format: uuid
example: '123e4567-e89b-12d3-a456-426655440000'
CloneComposeBody:
oneOf:
- $ref: '#/components/schemas/AWSEC2CloneCompose'
AWSEC2CloneCompose:
type: object
required:
- region
properties:
region:
type: string
share_with_accounts:
type: array
example: ['123456789012']
items:
type: string
CloneComposeResponse:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- id
properties:
id:
type: string
format: uuid
example: '123e4567-e89b-12d3-a456-426655440000'
CloneStatus:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- $ref: '#/components/schemas/UploadStatus'
parameters:
page:
name: page

View file

@ -539,7 +539,7 @@ func TestKojiJobTypeValidation(t *testing.T) {
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", finalizeID, path), ``, http.StatusOK, "*")
// The other IDs should fail
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", initID, path), ``, http.StatusNotFound, `{"code":"IMAGE-BUILDER-COMPOSER-26", "details": "", "href":"/api/image-builder-composer/v2/errors/26","id":"26","kind":"Error","reason":"Requested job has invalid type"}`, `operation_id`)
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", initID, path), ``, http.StatusNotFound, `{"code":"IMAGE-BUILDER-COMPOSER-26", "details": "", "href":"/api/image-builder-composer/v2/errors/26","id":"26","kind":"Error","reason":"Job with given id has an invalid type"}`, `operation_id`)
for _, buildID := range buildJobIDs {
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%s%s", buildID, path), ``, http.StatusOK, "*")

View file

@ -951,7 +951,7 @@ func TestComposeTargetErrors(t *testing.T) {
"ami": "",
"region": ""
},
"status": "",
"status": "failure",
"type": "aws"
}
},
@ -1198,3 +1198,132 @@ func TestImageTypes(t *testing.T) {
"kind": "ComposeId"
}`, "id")
}
func TestImageFromCompose(t *testing.T) {
srv, wrksrv, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
defer cancel()
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST", "/api/image-builder-composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_request":{
"architecture": "%s",
"image_type": "aws",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_options": {
"region": "eu-central-1"
}
}
}`, test_distro.TestDistroName, test_distro.TestArch3Name), http.StatusCreated, `
{
"href": "/api/image-builder-composer/v2/compose",
"kind": "ComposeId"
}`, "id")
jobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeOSBuild}, []string{""})
require.NoError(t, err)
require.Equal(t, worker.JobTypeOSBuild, jobType)
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/image-builder-composer/v2/composes/%v",
"kind": "ComposeStatus",
"id": "%v",
"image_status": {"status": "building"},
"status": "pending"
}`, jobId, jobId))
tr := target.NewAWSTargetResult(&target.AWSTargetResultOptions{
Ami: "ami-abc123",
Region: "eu-central-1",
})
res, err := json.Marshal(&worker.OSBuildJobResult{
Success: true,
OSBuildOutput: &osbuild.Result{Success: true},
TargetResults: []*target.TargetResult{
tr,
},
})
require.NoError(t, err)
err = wrksrv.FinishJob(token, res)
require.NoError(t, err)
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/composes/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/image-builder-composer/v2/composes/%v",
"kind": "ComposeStatus",
"id": "%v",
"status": "success",
"image_status": {
"status": "success",
"upload_status": {
"type": "aws",
"status": "success",
"options": {
"ami": "ami-abc123",
"region": "eu-central-1"
}
}
}
}`, jobId, jobId))
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST",
fmt.Sprintf("/api/image-builder-composer/v2/composes/%v/clone", jobId), `
{
"region": "eu-central-2",
"share_with_accounts": ["123456789012"]
}`, http.StatusCreated, fmt.Sprintf(`
{
"href": "/api/image-builder-composer/v2/composes/%v/clone",
"kind": "CloneComposeId"
}`, jobId), "id")
_, token, jobType, _, _, err = wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeAWSEC2Copy}, []string{""})
require.NoError(t, err)
require.Equal(t, worker.JobTypeAWSEC2Copy, jobType)
res, err = json.Marshal(&worker.AWSEC2CopyJobResult{
Ami: "ami-def456",
Region: "eu-central-2",
})
require.NoError(t, err)
err = wrksrv.FinishJob(token, res)
require.NoError(t, err)
imgJobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArch3Name, []string{worker.JobTypeAWSEC2Share}, []string{""})
require.NoError(t, err)
require.Equal(t, worker.JobTypeAWSEC2Share, jobType)
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", imgJobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/image-builder-composer/v2/clones/%v",
"kind": "CloneComposeStatus",
"id": "%v",
"status": "running",
"type": "aws"
}`, imgJobId, imgJobId), "options")
res, err = json.Marshal(&worker.AWSEC2ShareJobResult{
Ami: "ami-def456",
Region: "eu-central-2",
})
require.NoError(t, err)
err = wrksrv.FinishJob(token, res)
require.NoError(t, err)
test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "GET", fmt.Sprintf("/api/image-builder-composer/v2/clones/%v", imgJobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/image-builder-composer/v2/clones/%v",
"kind": "CloneComposeStatus",
"id": "%v",
"status": "success",
"type": "aws",
"options": {
"ami": "ami-def456",
"region": "eu-central-2"
}
}`, imgJobId, imgJobId))
}

View file

@ -284,7 +284,8 @@ curl \
# Prepare a request to be sent to the composer API.
#
REQUEST_FILE="${WORKDIR}/request.json"
REQUEST_FILE="${WORKDIR}/compose_request.json"
IMG_COMPOSE_REQ_FILE="${WORKDIR}/img_compose_request.json"
ARCH=$(uname -m)
SSH_USER=
TEST_ID="$(uuidgen)"
@ -418,6 +419,64 @@ function waitForState() {
export UPLOAD_OPTIONS
}
function sendImgFromCompose() {
OUTPUT=$(mktemp)
HTTPSTATUS=$(curl \
--silent \
--show-error \
--cacert /etc/osbuild-composer/ca-crt.pem \
--key /etc/osbuild-composer/client-key.pem \
--cert /etc/osbuild-composer/client-crt.pem \
--header 'Content-Type: application/json' \
--request POST \
--data @"$1" \
--write-out '%{http_code}' \
--output "$OUTPUT" \
https://localhost/api/image-builder-composer/v2/composes/"$COMPOSE_ID"/clone)
test "$HTTPSTATUS" = "201"
IMG_ID=$(jq -r '.id' "$OUTPUT")
}
function waitForImgState() {
while true
do
OUTPUT=$(curl \
--silent \
--show-error \
--cacert /etc/osbuild-composer/ca-crt.pem \
--key /etc/osbuild-composer/client-key.pem \
--cert /etc/osbuild-composer/client-crt.pem \
"https://localhost/api/image-builder-composer/v2/clones/$IMG_ID")
IMG_UPLOAD_STATUS=$(echo "$OUTPUT" | jq -r '.status')
IMG_UPLOAD_OPTIONS=$(echo "$OUTPUT" | jq -r '.options')
case "$IMG_UPLOAD_STATUS" in
"success")
break
;;
# all valid status values for a compose which hasn't finished yet
"pending"|"running")
;;
# default undesired state
"failure")
echo "Image compose failed"
exit 1
;;
*)
echo "API returned unexpected image status value: '$IMG_UPLOAD_STATUS'"
exit 1
;;
esac
sleep 30
done
# export for use in subcases
export IMG_UPLOAD_OPTIONS
}
#
# Make sure that requesting a non existing paquet results in failure
#
@ -427,7 +486,6 @@ jq '.customizations.packages = [ "jesuisunpaquetquinexistepas" ]' "$REQUEST_FILE
sendCompose "$REQUEST_FILE2"
waitForState "failure"
# crashed/stopped/killed worker should result in a failed state
sendCompose "$REQUEST_FILE"
waitForState "building"
@ -449,6 +507,12 @@ fi
test "$UPLOAD_TYPE" = "$EXPECTED_UPLOAD_TYPE"
test $((INIT_COMPOSES+1)) = "$SUBS_COMPOSES"
if [ -s "$IMG_COMPOSE_REQ_FILE" ]; then
sendImgFromCompose "$IMG_COMPOSE_REQ_FILE"
waitForImgState
fi
#
# Verify the Cloud-provider specific upload_status options
#

View file

@ -13,12 +13,17 @@ function cleanup() {
AWS_INSTANCE_ID="${AWS_INSTANCE_ID:-}"
AMI_IMAGE_ID="${AMI_IMAGE_ID:-}"
AWS_SNAPSHOT_ID="${AWS_SNAPSHOT_ID:-}"
AMI_ID_2="${AMI_ID_2:-}"
SNAPSHOT_ID_2="${SNAPSHOT_ID_2:-}"
if [ -n "$AWS_CMD" ]; then
$AWS_CMD ec2 terminate-instances --instance-ids "$AWS_INSTANCE_ID"
$AWS_CMD ec2 deregister-image --image-id "$AMI_IMAGE_ID"
$AWS_CMD ec2 delete-snapshot --snapshot-id "$AWS_SNAPSHOT_ID"
$AWS_CMD ec2 delete-key-pair --key-name "key-for-$AMI_IMAGE_ID"
$AWS_CMD ec2 deregister-image --region "$REGION_2" --image-id "$AMI_2"
$AWS_CMD ec2 delete-snapshot --region "$REGION_2" --snapshot-id "$SNAPSHOT_ID_2"
fi
}
@ -68,6 +73,13 @@ function createReqFile() {
}
}
}
EOF
cat > "$IMG_COMPOSE_REQ_FILE" <<EOF
{
"region": "${AWS_REGION_2}",
"share_with_accounts": ["${AWS_API_TEST_SHARE_ACCOUNT_2}"]
}
EOF
}
@ -122,6 +134,38 @@ function verify() {
exit 1
fi
# Verify that the 2nd image from the same compose was copied and shared with existing and new account
AMI_ID_2=$(echo "$IMG_UPLOAD_OPTIONS" | jq -r .ami)
REGION_2=$(echo "$IMG_UPLOAD_OPTIONS" | jq -r .region)
$AWS_CMD ec2 describe-images --owners self --region "$REGION_2" --image-ids "$AMI_ID_2" \
> "$WORKDIR/ami2.json"
SNAPSHOT_ID_2=$(jq -r '.Images[].BlockDeviceMappings[].Ebs.SnapshotId' "$WORKDIR/ami2.json")
$AWS_CMD ec2 describe-snapshot-attribute --region "$REGION_2" --snapshot-id "$SNAPSHOT_ID_2" \
--attribute createVolumePermission > "$WORKDIR/snapshot-attributes2.json"
SHARED_ID_2=$(jq -r ".CreateVolumePermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT\").UserId" "$WORKDIR/snapshot-attributes2.json")
if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID_2" ]; then
echo "EC2 Snapshot wasn't shared with AWS_API_TEST_SHARE_ACCOUNT"
exit 1
fi
SHARED_ID_2=$(jq -r ".CreateVolumePermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT_2\").UserId" "$WORKDIR/snapshot-attributes2.json")
if [ "$AWS_API_TEST_SHARE_ACCOUNT_2" != "$SHARED_ID_2" ]; then
echo "EC2 Snapshot wasn't shared with AWS_API_TEST_SHARE_ACCOUNT_2"
exit 1
fi
$AWS_CMD ec2 describe-image-attribute --attribute launchPermission --region "$REGION_2" --image-id "$AMI_ID_2" > "$WORKDIR/ami-attributes2.json"
SHARED_ID_2=$(jq -r ".LaunchPermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT\").UserId" "$WORKDIR/ami-attributes2.json")
if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID_2" ]; then
echo "EC2 ami wasn't shared with AWS_API_TEST_SHARE_ACCOUNT"
exit 1
fi
SHARED_ID_2=$(jq -r ".LaunchPermissions[] | select(.UserId==\"$AWS_API_TEST_SHARE_ACCOUNT_2\").UserId" "$WORKDIR/ami-attributes2.json")
if [ "$AWS_API_TEST_SHARE_ACCOUNT_2" != "$SHARED_ID_2" ]; then
echo "EC2 ami wasn't shared with AWS_API_TEST_SHARE_ACCOUNT_2"
exit 1
fi
# Create key-pair
$AWS_CMD ec2 create-key-pair --key-name "key-for-$AMI_IMAGE_ID" --query 'KeyMaterial' --output text > keypair.pem
chmod 400 ./keypair.pem