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:
parent
599829a3b8
commit
d13347e1ca
8 changed files with 794 additions and 117 deletions
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, "*")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue