cloudapi: V2

V2 is compliant with api.openshift.com design guidelines.

Errors are predefined, have codes, and are queryable.

All requests have an operationId set: a unique identifier which is
sortable by time. This is added to the response in case of an error.

All returned objects have the href, id, and kind field set.
This commit is contained in:
sanne 2021-08-25 16:49:17 +02:00 committed by Sanne Raymaekers
parent 19eb65e9fd
commit 5a9d8c792b
28 changed files with 4877 additions and 585 deletions

View file

@ -0,0 +1,251 @@
package v2
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
const (
ErrorCodePrefix = "COMPOSER-"
ErrorHREF = "/api/composer/v2/errors"
// ocm-sdk sends ErrorUnauthenticated with id 401 & code COMPOSER-401
ErrorUnauthenticated ServiceErrorCode = 401
ErrorUnauthorized ServiceErrorCode = 2
ErrorUnsupportedMediaType ServiceErrorCode = 3
ErrorUnsupportedDistribution ServiceErrorCode = 4
ErrorUnsupportedArchitecture ServiceErrorCode = 5
ErrorUnsupportedImageType ServiceErrorCode = 6
ErrorInvalidRepository ServiceErrorCode = 7
ErrorDNFError ServiceErrorCode = 8
ErrorInvalidOSTreeRef ServiceErrorCode = 9
ErrorInvalidOSTreeRepo ServiceErrorCode = 10
ErrorFailedToMakeManifest ServiceErrorCode = 11
ErrorInvalidUploadType ServiceErrorCode = 12
ErrorMultiImageCompose ServiceErrorCode = 13
ErrorInvalidComposeId ServiceErrorCode = 14
ErrorComposeNotFound ServiceErrorCode = 15
ErrorInvalidErrorId ServiceErrorCode = 16
ErrorErrorNotFound ServiceErrorCode = 17
ErrorInvalidPageParam ServiceErrorCode = 18
ErrorInvalidSizeParam ServiceErrorCode = 19
ErrorBodyDecodingError ServiceErrorCode = 20
ErrorResourceNotFound ServiceErrorCode = 21
ErrorMethodNotAllowed ServiceErrorCode = 22
ErrorNotAcceptable ServiceErrorCode = 23
// Internal errors, these are bugs
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
ErrorFailedToGenerateManifestSeed ServiceErrorCode = 1001
ErrorFailedToDepsolve ServiceErrorCode = 1002
ErrorJSONMarshallingError ServiceErrorCode = 1003
ErrorJSONUnMarshallingError ServiceErrorCode = 1004
ErrorEnqueueingJob ServiceErrorCode = 1005
ErrorSeveralUploadTargets ServiceErrorCode = 1006
ErrorUnknownUploadTarget ServiceErrorCode = 1007
ErrorFailedToLoadOpenAPISpec ServiceErrorCode = 1008
ErrorFailedToParseManifestVersion ServiceErrorCode = 1009
ErrorUnknownManifestVersion ServiceErrorCode = 1010
ErrorUnableToConvertOSTreeCommitStageMetadata ServiceErrorCode = 1011
ErrorMalformedOSBuildJobResult ServiceErrorCode = 1012
// Errors contained within this file
ErrorUnspecified ServiceErrorCode = 10000
ErrorNotHTTPError ServiceErrorCode = 10001
ErrorServiceErrorNotFound ServiceErrorCode = 10002
ErrorMalformedOperationID ServiceErrorCode = 10003
)
type ServiceErrorCode int
type serviceError struct {
code ServiceErrorCode
httpStatus int
reason string
}
type serviceErrors []serviceError
// Maps ServiceErrorcode to a reason and http code
func getServiceErrors() serviceErrors {
return serviceErrors{
serviceError{ErrorUnauthenticated, http.StatusUnauthorized, "Account authentication could not be verified"},
serviceError{ErrorUnauthorized, http.StatusForbidden, "Account is unauthorized to perform this action"},
serviceError{ErrorUnsupportedMediaType, http.StatusUnsupportedMediaType, "Only 'application/json' content is supported"},
serviceError{ErrorUnsupportedDistribution, http.StatusBadRequest, "Unsupported distribution"},
serviceError{ErrorUnsupportedArchitecture, http.StatusBadRequest, "Unsupported architecture"},
serviceError{ErrorUnsupportedImageType, http.StatusBadRequest, "Unsupported image type"},
serviceError{ErrorInvalidRepository, http.StatusBadRequest, "Must specify baseurl, mirrorlist, or metalink"},
serviceError{ErrorDNFError, http.StatusBadRequest, "Failed to depsolve packages"},
serviceError{ErrorInvalidOSTreeRef, http.StatusBadRequest, "Invalid OSTree ref"},
serviceError{ErrorInvalidOSTreeRepo, http.StatusBadRequest, "Error resolving OSTree repo"},
serviceError{ErrorFailedToMakeManifest, http.StatusBadRequest, "Failed to get manifest"},
serviceError{ErrorInvalidUploadType, http.StatusBadRequest, "Unknown upload request type"},
serviceError{ErrorMultiImageCompose, http.StatusBadRequest, "Only single-image composes are currently supported"},
serviceError{ErrorInvalidComposeId, http.StatusBadRequest, "Invalid format for compose id"},
serviceError{ErrorComposeNotFound, http.StatusNotFound, "Compose with given id not found"},
serviceError{ErrorInvalidErrorId, http.StatusBadRequest, "Invalid format for error id, it should be an integer as a string"},
serviceError{ErrorErrorNotFound, http.StatusNotFound, "Error with given id not found"},
serviceError{ErrorInvalidPageParam, http.StatusBadRequest, "Invalid format for page param, it should be an integer as a string"},
serviceError{ErrorInvalidSizeParam, http.StatusBadRequest, "Invalid format for size param, it should be an integer as a string"},
serviceError{ErrorBodyDecodingError, http.StatusBadRequest, "Malformed json, unable to decode body"},
serviceError{ErrorResourceNotFound, http.StatusNotFound, "Requested resource doesn't exist"},
serviceError{ErrorMethodNotAllowed, http.StatusMethodNotAllowed, "Requested method isn't supported for resource"},
serviceError{ErrorNotAcceptable, http.StatusNotAcceptable, "Only 'application/json' content is supported"},
serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
serviceError{ErrorFailedToDepsolve, http.StatusInternalServerError, "Failed to depsolve packages"},
serviceError{ErrorJSONMarshallingError, http.StatusInternalServerError, "Failed to marshal struct"},
serviceError{ErrorJSONUnMarshallingError, http.StatusInternalServerError, "Failed to unmarshal struct"},
serviceError{ErrorEnqueueingJob, http.StatusInternalServerError, "Failed to enqueue job"},
serviceError{ErrorSeveralUploadTargets, http.StatusInternalServerError, "Compose has more than one upload target"},
serviceError{ErrorUnknownUploadTarget, http.StatusInternalServerError, "Compose has unknown upload target"},
serviceError{ErrorFailedToLoadOpenAPISpec, http.StatusInternalServerError, "Unable to load openapi spec"},
serviceError{ErrorFailedToParseManifestVersion, http.StatusInternalServerError, "Unable to parse manifest version"},
serviceError{ErrorUnknownManifestVersion, http.StatusInternalServerError, "Unknown manifest version"},
serviceError{ErrorUnableToConvertOSTreeCommitStageMetadata, http.StatusInternalServerError, "Unable to convert ostree commit stage metadata"},
serviceError{ErrorMalformedOSBuildJobResult, http.StatusInternalServerError, "OSBuildJobResult does not have expected fields set"},
serviceError{ErrorUnspecified, http.StatusInternalServerError, "Unspecified internal error "},
serviceError{ErrorNotHTTPError, http.StatusInternalServerError, "Error is not an instance of HTTPError"},
serviceError{ErrorServiceErrorNotFound, http.StatusInternalServerError, "Error does not exist"},
serviceError{ErrorMalformedOperationID, http.StatusInternalServerError, "OperationID is empty or is not a string"},
}
}
func find(code ServiceErrorCode) *serviceError {
for _, e := range getServiceErrors() {
if e.code == code {
return &e
}
}
return &serviceError{ErrorServiceErrorNotFound, http.StatusInternalServerError, "Error does not exist"}
}
// Make an echo compatible error out of a service error
func HTTPError(code ServiceErrorCode) error {
return HTTPErrorWithInternal(code, nil)
}
// echo.HTTPError has a message interface{} field, which can be used to include the ServiceErrorCode
func HTTPErrorWithInternal(code ServiceErrorCode, internalErr error) error {
se := find(code)
he := echo.NewHTTPError(se.httpStatus, se.code)
if internalErr != nil {
he.Internal = internalErr
}
return he
}
// Convert a ServiceErrorCode into an Error as defined in openapi.v2.yml
// serviceError is optional, prevents multiple find() calls
func APIError(code ServiceErrorCode, serviceError *serviceError, c echo.Context) *Error {
se := serviceError
if se == nil {
se = find(code)
}
operationID, ok := c.Get("operationID").(string)
if !ok || operationID == "" {
se = find(ErrorMalformedOperationID)
}
return &Error{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("%s/%d", ErrorHREF, se.code),
Id: fmt.Sprintf("%d", se.code),
Kind: "Error",
},
Code: fmt.Sprintf("%s%d", ErrorCodePrefix, se.code),
OperationId: operationID, // set operation id from context
Reason: se.reason,
}
}
// Helper to make the ErrorList as defined in openapi.v2.yml
func APIErrorList(page int, pageSize int, c echo.Context) *ErrorList {
list := &ErrorList{
List: List{
Kind: "ErrorList",
Page: page,
Size: 0,
Total: len(getServiceErrors()),
},
Items: []Error{},
}
if page < 0 || pageSize < 0 {
return list
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
errs := getServiceErrors()[min(page*pageSize, len(getServiceErrors())):min(((page+1)*pageSize), len(getServiceErrors()))]
for _, e := range errs {
list.Items = append(list.Items, *APIError(e.code, &e, c))
}
list.Size = len(list.Items)
return list
}
func apiErrorFromEchoError(echoError *echo.HTTPError) ServiceErrorCode {
switch echoError.Code {
case http.StatusNotFound:
return ErrorResourceNotFound
case http.StatusMethodNotAllowed:
return ErrorMethodNotAllowed
case http.StatusNotAcceptable:
return ErrorNotAcceptable
default:
return ErrorUnspecified
}
}
// Convert an echo error into an AOC compliant one so we send a correct json error response
func (s *Server) HTTPErrorHandler(echoError error, c echo.Context) {
doResponse := func(code ServiceErrorCode, c echo.Context) {
if !c.Response().Committed {
var err error
sec := find(code)
apiErr := APIError(code, sec, c)
if sec.httpStatus == http.StatusInternalServerError {
c.Logger().Errorf("Internal server error. Code: %s, OperationId: %s", apiErr.Code, apiErr.OperationId)
}
if c.Request().Method == http.MethodHead {
err = c.NoContent(sec.httpStatus)
} else {
err = c.JSON(sec.httpStatus, apiErr)
}
if err != nil {
c.Logger().Errorf("Failed to return error response: %v", err)
}
} else {
c.Logger().Infof("Failed to return error response, response already committed: %d", code)
}
}
he, ok := echoError.(*echo.HTTPError)
if !ok {
doResponse(ErrorNotHTTPError, c)
return
}
sec, ok := he.Message.(ServiceErrorCode)
if !ok {
// No service code was set, so Echo threw this error
doResponse(apiErrorFromEchoError(he), c)
return
}
doResponse(sec, c)
}

View file

@ -0,0 +1,95 @@
package v2
import (
"fmt"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
)
func TestHTTPErrorReturnsEchoHTTPError(t *testing.T) {
for _, se := range getServiceErrors() {
err := HTTPError(se.code)
echoError, ok := err.(*echo.HTTPError)
require.True(t, ok)
require.Equal(t, se.httpStatus, echoError.Code)
serviceErrorCode, ok := echoError.Message.(ServiceErrorCode)
require.True(t, ok)
require.Equal(t, se.code, serviceErrorCode)
}
}
func TestAPIError(t *testing.T) {
e := echo.New()
for _, se := range getServiceErrors() {
ctx := e.NewContext(nil, nil)
ctx.Set("operationID", "test-operation-id")
apiError := APIError(se.code, nil, ctx)
require.Equal(t, fmt.Sprintf("/api/composer/v2/errors/%d", se.code), apiError.Href)
require.Equal(t, fmt.Sprintf("%d", se.code), apiError.Id)
require.Equal(t, "Error", apiError.Kind)
require.Equal(t, fmt.Sprintf("COMPOSER-%d", se.code), apiError.Code)
require.Equal(t, "test-operation-id", apiError.OperationId)
require.Equal(t, se.reason, apiError.Reason)
}
}
func TestAPIErrorOperationID(t *testing.T) {
ctx := echo.New().NewContext(nil, nil)
apiError := APIError(ErrorUnauthenticated, nil, ctx)
require.Equal(t, "COMPOSER-10003", apiError.Code)
ctx.Set("operationID", 5)
apiError = APIError(ErrorUnauthenticated, nil, ctx)
require.Equal(t, "COMPOSER-10003", apiError.Code)
ctx.Set("operationID", "test-operation-id")
apiError = APIError(ErrorUnauthenticated, nil, ctx)
require.Equal(t, "COMPOSER-401", apiError.Code)
}
func TestAPIErrorList(t *testing.T) {
ctx := echo.New().NewContext(nil, nil)
ctx.Set("operationID", "test-operation-id")
// negative values return empty list
errs := APIErrorList(-10, -30, ctx)
require.Equal(t, 0, errs.Size)
require.Equal(t, 0, len(errs.Items))
errs = APIErrorList(0, -30, ctx)
require.Equal(t, 0, errs.Size)
require.Equal(t, 0, len(errs.Items))
errs = APIErrorList(-10, 0, ctx)
require.Equal(t, 0, errs.Size)
require.Equal(t, 0, len(errs.Items))
// all of them
errs = APIErrorList(0, 1000, ctx)
require.Equal(t, len(getServiceErrors()), errs.Size)
// some of them
errs = APIErrorList(0, 10, ctx)
require.Equal(t, 10, errs.Size)
require.Equal(t, len(getServiceErrors()), errs.Total)
require.Equal(t, 0, errs.Page)
require.Equal(t, "COMPOSER-401", errs.Items[0].Code)
errs = APIErrorList(1, 10, ctx)
require.Equal(t, 10, errs.Size)
require.Equal(t, len(getServiceErrors()), errs.Total)
require.Equal(t, 1, errs.Page)
require.Equal(t, "COMPOSER-11", errs.Items[0].Code)
// high page
errs = APIErrorList(1000, 1, ctx)
require.Equal(t, 0, errs.Size)
require.Equal(t, len(getServiceErrors()), errs.Total)
require.Equal(t, 1000, errs.Page)
// zero pagesize
errs = APIErrorList(1000, 0, ctx)
require.Equal(t, 0, errs.Size)
require.Equal(t, len(getServiceErrors()), errs.Total)
require.Equal(t, 1000, errs.Page)
}

View file

@ -0,0 +1,573 @@
// Package v2 provides primitives to interact the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT.
package v2
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
"net/http"
"strings"
)
// AWSS3UploadRequestOptions defines model for AWSS3UploadRequestOptions.
type AWSS3UploadRequestOptions struct {
Region string `json:"region"`
S3 AWSUploadRequestOptionsS3 `json:"s3"`
}
// AWSS3UploadStatus defines model for AWSS3UploadStatus.
type AWSS3UploadStatus struct {
Url string `json:"url"`
}
// AWSUploadRequestOptions defines model for AWSUploadRequestOptions.
type AWSUploadRequestOptions struct {
Ec2 AWSUploadRequestOptionsEc2 `json:"ec2"`
Region string `json:"region"`
S3 AWSUploadRequestOptionsS3 `json:"s3"`
}
// AWSUploadRequestOptionsEc2 defines model for AWSUploadRequestOptionsEc2.
type AWSUploadRequestOptionsEc2 struct {
AccessKeyId string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"`
SnapshotName *string `json:"snapshot_name,omitempty"`
}
// AWSUploadRequestOptionsS3 defines model for AWSUploadRequestOptionsS3.
type AWSUploadRequestOptionsS3 struct {
AccessKeyId string `json:"access_key_id"`
Bucket string `json:"bucket"`
SecretAccessKey string `json:"secret_access_key"`
}
// AWSUploadStatus defines model for AWSUploadStatus.
type AWSUploadStatus struct {
Ami string `json:"ami"`
Region string `json:"region"`
}
// AzureUploadRequestOptions defines model for AzureUploadRequestOptions.
type AzureUploadRequestOptions struct {
// Name of the uploaded image. It must be unique in the given resource group.
// If name is omitted from the request, a random one based on a UUID is
// generated.
ImageName *string `json:"image_name,omitempty"`
// Location where the image should be uploaded and registered.
// How to list all locations:
// https://docs.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az_account_list_locations'
Location string `json:"location"`
// Name of the resource group where the image should be uploaded.
ResourceGroup string `json:"resource_group"`
// ID of subscription where the image should be uploaded.
SubscriptionId string `json:"subscription_id"`
// ID of the tenant where the image should be uploaded.
// How to find it in the Azure Portal:
// https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant
TenantId string `json:"tenant_id"`
}
// AzureUploadStatus defines model for AzureUploadStatus.
type AzureUploadStatus struct {
ImageName string `json:"image_name"`
}
// ComposeId defines model for ComposeId.
type ComposeId struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
Id string `json:"id"`
}
// ComposeMetadata defines model for ComposeMetadata.
type ComposeMetadata struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
// ID (hash) of the built commit
OstreeCommit *string `json:"ostree_commit,omitempty"`
// Package list including NEVRA
Packages *[]PackageMetadata `json:"packages,omitempty"`
}
// ComposeRequest defines model for ComposeRequest.
type ComposeRequest struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
Customizations *Customizations `json:"customizations,omitempty"`
Distribution string `json:"distribution"`
ImageRequests []ImageRequest `json:"image_requests"`
}
// ComposeStatus defines model for ComposeStatus.
type ComposeStatus struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
ImageStatus ImageStatus `json:"image_status"`
}
// Customizations defines model for Customizations.
type Customizations struct {
Packages *[]string `json:"packages,omitempty"`
Subscription *Subscription `json:"subscription,omitempty"`
Users *[]User `json:"users,omitempty"`
}
// Error defines model for Error.
type Error struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
Code string `json:"code"`
OperationId string `json:"operation_id"`
Reason string `json:"reason"`
}
// ErrorList defines model for ErrorList.
type ErrorList struct {
// Embedded struct due to allOf(#/components/schemas/List)
List
// Embedded fields due to inline allOf schema
Items []Error `json:"items"`
}
// GCPUploadRequestOptions defines model for GCPUploadRequestOptions.
type GCPUploadRequestOptions struct {
// Name of an existing STANDARD Storage class Bucket.
Bucket string `json:"bucket"`
// The name to use for the imported and shared Compute Engine image.
// The image name must be unique within the GCP project, which is used
// for the OS image upload and import. If not specified a random
// 'composer-api-<uuid>' string is used as the image name.
ImageName *string `json:"image_name,omitempty"`
// The GCP region where the OS image will be imported to and shared from.
// The value must be a valid GCP location. See https://cloud.google.com/storage/docs/locations.
// If not specified, the multi-region location closest to the source
// (source Storage Bucket location) is chosen automatically.
Region *string `json:"region,omitempty"`
// List of valid Google accounts to share the imported Compute Engine image with.
// Each string must contain a specifier of the account type. Valid formats are:
// - 'user:{emailid}': An email address that represents a specific
// Google account. For example, 'alice@example.com'.
// - 'serviceAccount:{emailid}': An email address that represents a
// service account. For example, 'my-other-app@appspot.gserviceaccount.com'.
// - 'group:{emailid}': An email address that represents a Google group.
// For example, 'admins@example.com'.
// - 'domain:{domain}': The G Suite domain (primary) that represents all
// the users of that domain. For example, 'google.com' or 'example.com'.
// If not specified, the imported Compute Engine image is not shared with any
// account.
ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"`
}
// GCPUploadStatus defines model for GCPUploadStatus.
type GCPUploadStatus struct {
ImageName string `json:"image_name"`
ProjectId string `json:"project_id"`
}
// ImageRequest defines model for ImageRequest.
type ImageRequest struct {
Architecture string `json:"architecture"`
ImageType string `json:"image_type"`
Ostree *OSTree `json:"ostree,omitempty"`
Repositories []Repository `json:"repositories"`
UploadRequest UploadRequest `json:"upload_request"`
}
// ImageStatus defines model for ImageStatus.
type ImageStatus struct {
Status ImageStatusValue `json:"status"`
UploadStatus *UploadStatus `json:"upload_status,omitempty"`
}
// ImageStatusValue defines model for ImageStatusValue.
type ImageStatusValue string
// List of ImageStatusValue
const (
ImageStatusValue_building ImageStatusValue = "building"
ImageStatusValue_failure ImageStatusValue = "failure"
ImageStatusValue_pending ImageStatusValue = "pending"
ImageStatusValue_registering ImageStatusValue = "registering"
ImageStatusValue_success ImageStatusValue = "success"
ImageStatusValue_uploading ImageStatusValue = "uploading"
)
// List defines model for List.
type List struct {
Kind string `json:"kind"`
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
}
// OSTree defines model for OSTree.
type OSTree struct {
Ref *string `json:"ref,omitempty"`
Url *string `json:"url,omitempty"`
}
// ObjectReference defines model for ObjectReference.
type ObjectReference struct {
Href string `json:"href"`
Id string `json:"id"`
Kind string `json:"kind"`
}
// PackageMetadata defines model for PackageMetadata.
type PackageMetadata struct {
Arch string `json:"arch"`
Epoch *string `json:"epoch,omitempty"`
Name string `json:"name"`
Release string `json:"release"`
Sigmd5 string `json:"sigmd5"`
Signature *string `json:"signature,omitempty"`
Type string `json:"type"`
Version string `json:"version"`
}
// Repository defines model for Repository.
type Repository struct {
Baseurl *string `json:"baseurl,omitempty"`
Metalink *string `json:"metalink,omitempty"`
Mirrorlist *string `json:"mirrorlist,omitempty"`
Rhsm bool `json:"rhsm"`
}
// Subscription defines model for Subscription.
type Subscription struct {
ActivationKey string `json:"activation_key"`
BaseUrl string `json:"base_url"`
Insights bool `json:"insights"`
Organization string `json:"organization"`
ServerUrl string `json:"server_url"`
}
// UploadRequest defines model for UploadRequest.
type UploadRequest struct {
Options interface{} `json:"options"`
Type UploadTypes `json:"type"`
}
// UploadStatus defines model for UploadStatus.
type UploadStatus struct {
Options interface{} `json:"options"`
Status string `json:"status"`
Type UploadTypes `json:"type"`
}
// UploadTypes defines model for UploadTypes.
type UploadTypes string
// List of UploadTypes
const (
UploadTypes_aws UploadTypes = "aws"
UploadTypes_aws_s3 UploadTypes = "aws.s3"
UploadTypes_azure UploadTypes = "azure"
UploadTypes_gcp UploadTypes = "gcp"
)
// User defines model for User.
type User struct {
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
ObjectReference
// Embedded fields due to inline allOf schema
Groups *[]string `json:"groups,omitempty"`
Key *string `json:"key,omitempty"`
Name string `json:"name"`
}
// Page defines model for page.
type Page string
// Size defines model for size.
type Size string
// PostComposeJSONBody defines parameters for PostCompose.
type PostComposeJSONBody ComposeRequest
// GetErrorListParams defines parameters for GetErrorList.
type GetErrorListParams struct {
// Page index
Page *Page `json:"page,omitempty"`
// Number of items in each page
Size *Size `json:"size,omitempty"`
}
// PostComposeRequestBody defines body for PostCompose for application/json ContentType.
type PostComposeJSONRequestBody PostComposeJSONBody
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Create compose
// (POST /compose)
PostCompose(ctx echo.Context) error
// The status of a compose
// (GET /compose/{id})
GetComposeStatus(ctx echo.Context, id string) error
// Get the metadata for a compose.
// (GET /compose/{id}/metadata)
GetComposeMetadata(ctx echo.Context, id string) error
// Get a list of all possible errors
// (GET /errors)
GetErrorList(ctx echo.Context, params GetErrorListParams) error
// Get error description
// (GET /errors/{id})
GetError(ctx echo.Context, id string) error
// Get the openapi spec in json format
// (GET /openapi)
GetOpenapi(ctx echo.Context) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
}
// PostCompose converts echo context to params.
func (w *ServerInterfaceWrapper) PostCompose(ctx echo.Context) error {
var err error
ctx.Set("Bearer.Scopes", []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.PostCompose(ctx)
return err
}
// GetComposeStatus converts echo context to params.
func (w *ServerInterfaceWrapper) GetComposeStatus(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id string
err = runtime.BindStyledParameter("simple", false, "id", ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
ctx.Set("Bearer.Scopes", []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetComposeStatus(ctx, id)
return err
}
// GetComposeMetadata converts echo context to params.
func (w *ServerInterfaceWrapper) GetComposeMetadata(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id string
err = runtime.BindStyledParameter("simple", false, "id", ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
ctx.Set("Bearer.Scopes", []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetComposeMetadata(ctx, id)
return err
}
// GetErrorList converts echo context to params.
func (w *ServerInterfaceWrapper) GetErrorList(ctx echo.Context) error {
var err error
ctx.Set("Bearer.Scopes", []string{""})
// Parameter object where we will unmarshal all parameters from the context
var params GetErrorListParams
// ------------- Optional query parameter "page" -------------
err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), &params.Page)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err))
}
// ------------- Optional query parameter "size" -------------
err = runtime.BindQueryParameter("form", true, false, "size", ctx.QueryParams(), &params.Size)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter size: %s", err))
}
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetErrorList(ctx, params)
return err
}
// GetError converts echo context to params.
func (w *ServerInterfaceWrapper) GetError(ctx echo.Context) error {
var err error
// ------------- Path parameter "id" -------------
var id string
err = runtime.BindStyledParameter("simple", false, "id", ctx.Param("id"), &id)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
}
ctx.Set("Bearer.Scopes", []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetError(ctx, id)
return err
}
// GetOpenapi converts echo context to params.
func (w *ServerInterfaceWrapper) GetOpenapi(ctx echo.Context) error {
var err error
ctx.Set("Bearer.Scopes", []string{""})
// Invoke the callback with all the unmarshalled arguments
err = w.Handler.GetOpenapi(ctx)
return err
}
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}
// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
wrapper := ServerInterfaceWrapper{
Handler: si,
}
router.POST("/compose", wrapper.PostCompose)
router.GET("/compose/:id", wrapper.GetComposeStatus)
router.GET("/compose/:id/metadata", wrapper.GetComposeMetadata)
router.GET("/errors", wrapper.GetErrorList)
router.GET("/errors/:id", wrapper.GetError)
router.GET("/openapi", wrapper.GetOpenapi)
}
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
}
// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file.
func GetSwagger() (*openapi3.Swagger, error) {
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
if err != nil {
return nil, fmt.Errorf("error base64 decoding spec: %s", err)
}
zr, err := gzip.NewReader(bytes.NewReader(zipped))
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %s", err)
}
var buf bytes.Buffer
_, err = buf.ReadFrom(zr)
if err != nil {
return nil, fmt.Errorf("error decompressing spec: %s", err)
}
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("error loading Swagger: %s", err)
}
return swagger, nil
}

View file

@ -0,0 +1,798 @@
---
openapi: 3.0.1
info:
version: '2'
title: OSBuild Composer cloud api
description: Service to build and install images.
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: https://api.openshift.com/api/composer/v2
description: Main (production) server
- url: https://api.stage.openshift.com/api/composer/v2
description: Staging server
- url: /api/composer/v2
description: current domain
paths:
/openapi:
get:
operationId: getOpenapi
summary: Get the openapi spec in json format
security:
- Bearer: []
responses:
'200':
description: openapi spec in json format
'500':
description: Unexpected error occurred
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/compose/{id}:
get:
operationId: getComposeStatus
summary: The status of a compose
security:
- Bearer: []
parameters:
- in: path
name: id
schema:
type: string
format: uuid
example: '123e4567-e89b-12d3-a456-426655440000'
required: true
description: ID of compose status to get
description: |-
Get the status of a running or completed compose.
This includes whether or not the compose succeeded.
responses:
'200':
description: compose status
content:
application/json:
schema:
$ref: '#/components/schemas/ComposeStatus'
'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/{id}/metadata:
get:
operationId: getComposeMetadata
summary: Get the metadata for a compose.
security:
- Bearer: []
parameters:
- in: path
name: id
schema:
type: string
format: uuid
example: 123e4567-e89b-12d3-a456-426655440000
required: true
description: ID of compose status to get
description: |-
Get the metadata of a finished compose.
The exact information returned depends on the requested image type.
responses:
'200':
description: The metadata for the given compose.
content:
application/json:
schema:
$ref: '#/components/schemas/ComposeMetadata'
'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
summary: Create compose
description: Create a new compose, potentially consisting of several images and upload each to their destinations.
security:
- Bearer: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ComposeRequest'
responses:
'201':
description: Compose has started
content:
application/json:
schema:
$ref: '#/components/schemas/ComposeId'
'400':
description: Invalid compose request
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'
/errors/{id}:
get:
operationId: getError
summary: Get error description
description: Get an instance of the error specified by id
security:
- Bearer: []
parameters:
- in: path
name: id
schema:
type: string
example: '13'
required: true
description: ID of the error
responses:
'200':
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 error id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Unexpected error occurred
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/errors:
get:
operationId: getErrorList
summary: Get a list of all possible errors
security:
- Bearer: []
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: A list of errors
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorList'
'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 error id
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Unexpected error occurred
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
ObjectReference:
type: object
required:
- id
- kind
- href
properties:
id:
type: string
kind:
type: string
href:
type: string
List:
type: object
properties:
kind:
type: string
page:
type: integer
size:
type: integer
total:
type: integer
required:
- kind
- page
- size
- total
- items
Error:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- code
- reason
- operation_id
properties:
code:
type: string
reason:
type: string
operation_id:
type: string
ErrorList:
allOf:
- $ref: '#/components/schemas/List'
- type: object
required:
- items
properties:
items:
type: array
items:
$ref: '#/components/schemas/Error'
ComposeStatus:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- image_status
properties:
image_status:
$ref: '#/components/schemas/ImageStatus'
ImageStatus:
required:
- status
properties:
status:
$ref: '#/components/schemas/ImageStatusValue'
upload_status:
$ref: '#/components/schemas/UploadStatus'
ImageStatusValue:
type: string
enum: ['success', 'failure', 'pending', 'building', 'uploading', 'registering']
UploadStatus:
required:
- status
- type
- options
properties:
status:
type: string
enum: ['success', 'failure', 'pending', 'running']
type:
$ref: '#/components/schemas/UploadTypes'
options:
oneOf:
- $ref: '#/components/schemas/AWSUploadStatus'
- $ref: '#/components/schemas/AWSS3UploadStatus'
- $ref: '#/components/schemas/GCPUploadStatus'
- $ref: '#/components/schemas/AzureUploadStatus'
AWSUploadStatus:
type: object
required:
- ami
- region
properties:
ami:
type: string
example: 'ami-0c830793775595d4b'
region:
type: string
example: 'eu-west-1'
AWSS3UploadStatus:
type: object
required:
- url
properties:
url:
type: string
GCPUploadStatus:
type: object
required:
- project_id
- image_name
properties:
project_id:
type: string
example: 'ascendant-braid-303513'
image_name:
type: string
example: 'my-image'
AzureUploadStatus:
type: object
required:
- image_name
properties:
image_name:
type: string
example: 'my-image'
ComposeMetadata:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
properties:
packages:
type: array
items:
$ref: '#/components/schemas/PackageMetadata'
description: 'Package list including NEVRA'
ostree_commit:
type: string
description: 'ID (hash) of the built commit'
PackageMetadata:
required:
- type
- name
- version
- release
- arch
- sigmd5
properties:
type:
type: string
name:
type: string
version:
type: string
release:
type: string
epoch:
type: string
arch:
type: string
sigmd5:
type: string
signature:
type: string
ComposeRequest:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- distribution
- image_requests
properties:
distribution:
type: string
example: 'rhel-8'
image_requests:
type: array
items:
$ref: '#/components/schemas/ImageRequest'
customizations:
$ref: '#/components/schemas/Customizations'
ImageRequest:
required:
- architecture
- image_type
- repositories
- upload_request
properties:
architecture:
type: string
example: 'x86_64'
image_type:
type: string
example: 'ami'
repositories:
type: array
items:
$ref: '#/components/schemas/Repository'
ostree:
$ref: '#/components/schemas/OSTree'
upload_request:
$ref: '#/components/schemas/UploadRequest'
Repository:
type: object
required:
- rhsm
properties:
rhsm:
type: boolean
baseurl:
type: string
format: url
example: 'https://cdn.redhat.com/content/dist/rhel8/8/x86_64/baseos/os/'
mirrorlist:
type: string
format: url
example: 'http://mirrorlist.centos.org/?release=8-stream&arch=aarch64&repo=BaseOS'
metalink:
type: string
format: url
example: 'https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=x86_64'
UploadRequest:
type: object
required:
- type
- options
properties:
type:
$ref: '#/components/schemas/UploadTypes'
options:
oneOf:
- $ref: '#/components/schemas/AWSUploadRequestOptions'
- $ref: '#/components/schemas/AWSS3UploadRequestOptions'
- $ref: '#/components/schemas/GCPUploadRequestOptions'
- $ref: '#/components/schemas/AzureUploadRequestOptions'
UploadTypes:
type: string
enum: ['aws', 'aws.s3', 'gcp', 'azure']
AWSUploadRequestOptions:
type: object
required:
- region
- s3
- ec2
properties:
region:
type: string
example: 'eu-west-1'
s3:
$ref: '#/components/schemas/AWSUploadRequestOptionsS3'
ec2:
$ref: '#/components/schemas/AWSUploadRequestOptionsEc2'
AWSS3UploadRequestOptions:
type: object
required:
- region
- s3
properties:
region:
type: string
example: 'eu-west-1'
s3:
$ref: '#/components/schemas/AWSUploadRequestOptionsS3'
AWSUploadRequestOptionsS3:
type: object
required:
- access_key_id
- secret_access_key
- bucket
properties:
access_key_id:
type: string
example: 'AKIAIOSFODNN7EXAMPLE'
secret_access_key:
type: string
format: password
example: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
bucket:
type: string
example: 'my-bucket'
AWSUploadRequestOptionsEc2:
type: object
required:
- access_key_id
- secret_access_key
properties:
access_key_id:
type: string
example: 'AKIAIOSFODNN7EXAMPLE'
secret_access_key:
type: string
format: password
example: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
snapshot_name:
type: string
example: 'my-snapshot'
share_with_accounts:
type: array
example: ['123456789012']
items:
type: string
GCPUploadRequestOptions:
type: object
required:
- bucket
properties:
region:
type: string
example: 'eu'
description: |
The GCP region where the OS image will be imported to and shared from.
The value must be a valid GCP location. See https://cloud.google.com/storage/docs/locations.
If not specified, the multi-region location closest to the source
(source Storage Bucket location) is chosen automatically.
bucket:
type: string
example: 'my-example-bucket'
description: 'Name of an existing STANDARD Storage class Bucket.'
# don't expose the os type for now
# os:
# type: string
# example: 'rhel-8-byol'
# description: 'OS of the disk image being imported needed for installation of GCP guest tools.'
image_name:
type: string
example: 'my-image'
description: |
The name to use for the imported and shared Compute Engine image.
The image name must be unique within the GCP project, which is used
for the OS image upload and import. If not specified a random
'composer-api-<uuid>' string is used as the image name.
share_with_accounts:
type: array
example: [
'user:alice@example.com',
'serviceAccount:my-other-app@appspot.gserviceaccount.com',
'group:admins@example.com',
'domain:example.com'
]
description: |
List of valid Google accounts to share the imported Compute Engine image with.
Each string must contain a specifier of the account type. Valid formats are:
- 'user:{emailid}': An email address that represents a specific
Google account. For example, 'alice@example.com'.
- 'serviceAccount:{emailid}': An email address that represents a
service account. For example, 'my-other-app@appspot.gserviceaccount.com'.
- 'group:{emailid}': An email address that represents a Google group.
For example, 'admins@example.com'.
- 'domain:{domain}': The G Suite domain (primary) that represents all
the users of that domain. For example, 'google.com' or 'example.com'.
If not specified, the imported Compute Engine image is not shared with any
account.
items:
type: string
AzureUploadRequestOptions:
type: object
required:
- tenant_id
- subscription_id
- resource_group
- location
properties:
tenant_id:
type: string
example: '5c7ef5b6-1c3f-4da0-a622-0b060239d7d7'
description: |
ID of the tenant where the image should be uploaded.
How to find it in the Azure Portal:
https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant
subscription_id:
type: string
example: '4e5d8b2c-ab24-4413-90c5-612306e809e2'
description: |
ID of subscription where the image should be uploaded.
resource_group:
type: string
example: 'ToucanResourceGroup'
description: |
Name of the resource group where the image should be uploaded.
location:
type: string
example: 'westeurope'
description: |
Location where the image should be uploaded and registered.
How to list all locations:
https://docs.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az_account_list_locations'
image_name:
type: string
example: 'my-image'
description: |
Name of the uploaded image. It must be unique in the given resource group.
If name is omitted from the request, a random one based on a UUID is
generated.
Customizations:
type: object
properties:
subscription:
$ref: '#/components/schemas/Subscription'
packages:
type: array
example: ['postgres']
items:
type: string
users:
type: array
items:
$ref: '#/components/schemas/User'
OSTree:
type: object
properties:
url:
type: string
ref:
type: string
example: 'rhel/8/x86_64/edge'
Subscription:
type: object
required:
- organization
- activation_key
- server_url
- base_url
- insights
properties:
organization:
type: string
example: '2040324'
activation_key:
type: string
format: password
example: 'my-secret-key'
server_url:
type: string
example: 'subscription.rhsm.redhat.com'
base_url:
type: string
format: url
example: 'http://cdn.redhat.com/'
insights:
type: boolean
example: true
User:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- name
properties:
name:
type: string
example: "user1"
groups:
type: array
items:
type: string
example: "group1"
key:
type: string
example: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrGKErMYi+MMUwuHaRAJmRLoIzRf2qD2dD5z0BTx/6x"
ComposeId:
allOf:
- $ref: '#/components/schemas/ObjectReference'
- type: object
required:
- id
properties:
id:
type: string
format: uuid
example: '123e4567-e89b-12d3-a456-426655440000'
parameters:
page:
name: page
in: query
description: Page index
required: false
schema:
type: string
examples:
page:
value: "1"
size:
name: size
in: query
description: Number of items in each page
required: false
schema:
type: string
examples:
size:
value: "100"
securitySchemes:
Bearer:
scheme: bearer
bearerFormat: JWT
type: http

639
internal/cloudapi/v2/v2.go Normal file
View file

@ -0,0 +1,639 @@
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --package=v2 --generate types,spec,server -o openapi.v2.gen.go openapi.v2.yml
package v2
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log"
"math"
"math/big"
"net/http"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/distroregistry"
"github.com/osbuild/osbuild-composer/internal/osbuild1"
"github.com/osbuild/osbuild-composer/internal/ostree"
"github.com/osbuild/osbuild-composer/internal/prometheus"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/worker"
)
// Server represents the state of the cloud Server
type Server struct {
logger *log.Logger
workers *worker.Server
rpmMetadata rpmmd.RPMMD
distros *distroregistry.Registry
}
type apiHandlers struct {
server *Server
}
type binder struct{}
func NewServer(logger *log.Logger, workers *worker.Server, rpmMetadata rpmmd.RPMMD, distros *distroregistry.Registry) *Server {
server := &Server{
workers: workers,
rpmMetadata: rpmMetadata,
distros: distros,
}
return server
}
func (server *Server) Handler(path string) http.Handler {
e := echo.New()
e.Binder = binder{}
e.HTTPErrorHandler = server.HTTPErrorHandler
e.StdLogger = server.logger
e.Pre(common.OperationIDMiddleware)
handler := apiHandlers{
server: server,
}
RegisterHandlers(e.Group(path, server.IncRequests), &handler)
return e
}
func (b binder) Bind(i interface{}, ctx echo.Context) error {
contentType := ctx.Request().Header["Content-Type"]
if len(contentType) != 1 || contentType[0] != "application/json" {
return HTTPError(ErrorUnsupportedMediaType)
}
err := json.NewDecoder(ctx.Request().Body).Decode(i)
if err != nil {
return HTTPErrorWithInternal(ErrorBodyDecodingError, err)
}
return nil
}
func (s *Server) IncRequests(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
prometheus.TotalRequests.Inc()
if strings.HasSuffix(c.Path(), "/compose") {
prometheus.ComposeRequests.Inc()
}
return next(c)
}
}
func (h *apiHandlers) GetOpenapi(ctx echo.Context) error {
spec, err := GetSwagger()
if err != nil {
return HTTPError(ErrorFailedToLoadOpenAPISpec)
}
return ctx.JSON(http.StatusOK, spec)
}
func (h *apiHandlers) GetErrorList(ctx echo.Context, params GetErrorListParams) error {
page := 0
var err error
if params.Page != nil {
page, err = strconv.Atoi(string(*params.Page))
if err != nil {
return HTTPError(ErrorInvalidPageParam)
}
}
size := 100
if params.Size != nil {
size, err = strconv.Atoi(string(*params.Size))
if err != nil {
return HTTPError(ErrorInvalidSizeParam)
}
}
return ctx.JSON(http.StatusOK, APIErrorList(page, size, ctx))
}
func (h *apiHandlers) GetError(ctx echo.Context, id string) error {
errorId, err := strconv.Atoi(id)
if err != nil {
return HTTPError(ErrorInvalidErrorId)
}
apiError := APIError(ServiceErrorCode(errorId), nil, ctx)
// If the service error wasn't found, it's a 404 in this instance
if apiError.Id == fmt.Sprintf("%d", ErrorServiceErrorNotFound) {
return HTTPError(ErrorErrorNotFound)
}
return ctx.JSON(http.StatusOK, apiError)
}
func (h *apiHandlers) PostCompose(ctx echo.Context) error {
var request ComposeRequest
err := ctx.Bind(&request)
if err != nil {
return err
}
distribution := h.server.distros.GetDistro(request.Distribution)
if distribution == nil {
return HTTPError(ErrorUnsupportedDistribution)
}
var bp = blueprint.Blueprint{}
err = bp.Initialize()
if err != nil {
return HTTPError(ErrorFailedToInitializeBlueprint)
}
if request.Customizations != nil && request.Customizations.Packages != nil {
for _, p := range *request.Customizations.Packages {
bp.Packages = append(bp.Packages, blueprint.Package{
Name: p,
})
}
}
type imageRequest struct {
manifest distro.Manifest
arch string
exports []string
}
imageRequests := make([]imageRequest, len(request.ImageRequests))
var targets []*target.Target
// use the same seed for all images so we get the same IDs
bigSeed, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return HTTPError(ErrorFailedToGenerateManifestSeed)
}
manifestSeed := bigSeed.Int64()
for i, ir := range request.ImageRequests {
arch, err := distribution.GetArch(ir.Architecture)
if err != nil {
return HTTPError(ErrorUnsupportedArchitecture)
}
imageType, err := arch.GetImageType(ir.ImageType)
if err != nil {
return HTTPError(ErrorUnsupportedImageType)
}
repositories := make([]rpmmd.RepoConfig, len(ir.Repositories))
for j, repo := range ir.Repositories {
repositories[j].RHSM = repo.Rhsm
if repo.Baseurl != nil {
repositories[j].BaseURL = *repo.Baseurl
} else if repo.Mirrorlist != nil {
repositories[j].MirrorList = *repo.Mirrorlist
} else if repo.Metalink != nil {
repositories[j].Metalink = *repo.Metalink
} else {
return HTTPError(ErrorInvalidRepository)
}
}
packageSets := imageType.PackageSets(bp)
pkgSpecSets := make(map[string][]rpmmd.PackageSpec)
for name, packages := range packageSets {
pkgs, _, err := h.server.rpmMetadata.Depsolve(packages, repositories, distribution.ModulePlatformID(), arch.Name(), distribution.Releasever())
var dnfError *rpmmd.DNFError
if err != nil && errors.As(err, &dnfError) {
return HTTPError(ErrorDNFError)
} else if err != nil {
return HTTPError(ErrorFailedToDepsolve)
}
pkgSpecSets[name] = pkgs
}
imageOptions := distro.ImageOptions{Size: imageType.Size(0)}
if request.Customizations != nil && request.Customizations.Subscription != nil {
imageOptions.Subscription = &distro.SubscriptionImageOptions{
Organization: request.Customizations.Subscription.Organization,
ActivationKey: request.Customizations.Subscription.ActivationKey,
ServerUrl: request.Customizations.Subscription.ServerUrl,
BaseUrl: request.Customizations.Subscription.BaseUrl,
Insights: request.Customizations.Subscription.Insights,
}
}
// set default ostree ref, if one not provided
ostreeOptions := ir.Ostree
if ostreeOptions == nil || ostreeOptions.Ref == nil {
imageOptions.OSTree = distro.OSTreeImageOptions{Ref: imageType.OSTreeRef()}
} else if !ostree.VerifyRef(*ostreeOptions.Ref) {
return HTTPError(ErrorInvalidOSTreeRef)
} else {
imageOptions.OSTree = distro.OSTreeImageOptions{Ref: *ostreeOptions.Ref}
}
var parent string
if ostreeOptions != nil && ostreeOptions.Url != nil {
imageOptions.OSTree.URL = *ostreeOptions.Url
parent, err = ostree.ResolveRef(imageOptions.OSTree.URL, imageOptions.OSTree.Ref)
if err != nil {
return HTTPError(ErrorInvalidOSTreeRepo)
}
imageOptions.OSTree.Parent = parent
}
// Set the blueprint customisation to take care of the user
var blueprintCustoms *blueprint.Customizations
if request.Customizations != nil && request.Customizations.Users != nil {
var userCustomizations []blueprint.UserCustomization
for _, user := range *request.Customizations.Users {
var groups []string
if user.Groups != nil {
groups = *user.Groups
} else {
groups = nil
}
userCustomizations = append(userCustomizations,
blueprint.UserCustomization{
Name: user.Name,
Key: user.Key,
Groups: groups,
},
)
}
blueprintCustoms = &blueprint.Customizations{
User: userCustomizations,
}
}
manifest, err := imageType.Manifest(blueprintCustoms, imageOptions, repositories, pkgSpecSets, manifestSeed)
if err != nil {
return HTTPError(ErrorFailedToMakeManifest)
}
imageRequests[i].manifest = manifest
imageRequests[i].arch = arch.Name()
imageRequests[i].exports = imageType.Exports()
uploadRequest := ir.UploadRequest
/* oneOf is not supported by the openapi generator so marshal and unmarshal the uploadrequest based on the type */
if uploadRequest.Type == UploadTypes_aws {
var awsUploadOptions AWSUploadRequestOptions
jsonUploadOptions, err := json.Marshal(uploadRequest.Options)
if err != nil {
return HTTPError(ErrorJSONMarshallingError)
}
err = json.Unmarshal(jsonUploadOptions, &awsUploadOptions)
if err != nil {
return HTTPError(ErrorJSONUnMarshallingError)
}
var share []string
if awsUploadOptions.Ec2.ShareWithAccounts != nil {
share = *awsUploadOptions.Ec2.ShareWithAccounts
}
key := fmt.Sprintf("composer-api-%s", uuid.New().String())
t := target.NewAWSTarget(&target.AWSTargetOptions{
Filename: imageType.Filename(),
Region: awsUploadOptions.Region,
AccessKeyID: awsUploadOptions.S3.AccessKeyId,
SecretAccessKey: awsUploadOptions.S3.SecretAccessKey,
Bucket: awsUploadOptions.S3.Bucket,
Key: key,
ShareWithAccounts: share,
})
if awsUploadOptions.Ec2.SnapshotName != nil {
t.ImageName = *awsUploadOptions.Ec2.SnapshotName
} else {
t.ImageName = key
}
targets = append(targets, t)
} else if uploadRequest.Type == UploadTypes_aws_s3 {
var awsS3UploadOptions AWSS3UploadRequestOptions
jsonUploadOptions, err := json.Marshal(uploadRequest.Options)
if err != nil {
return HTTPError(ErrorJSONMarshallingError)
}
err = json.Unmarshal(jsonUploadOptions, &awsS3UploadOptions)
if err != nil {
return HTTPError(ErrorJSONUnMarshallingError)
}
key := fmt.Sprintf("composer-api-%s", uuid.New().String())
t := target.NewAWSS3Target(&target.AWSS3TargetOptions{
Filename: imageType.Filename(),
Region: awsS3UploadOptions.Region,
AccessKeyID: awsS3UploadOptions.S3.AccessKeyId,
SecretAccessKey: awsS3UploadOptions.S3.SecretAccessKey,
Bucket: awsS3UploadOptions.S3.Bucket,
Key: key,
})
t.ImageName = key
targets = append(targets, t)
} else if uploadRequest.Type == UploadTypes_gcp {
var gcpUploadOptions GCPUploadRequestOptions
jsonUploadOptions, err := json.Marshal(uploadRequest.Options)
if err != nil {
return HTTPError(ErrorJSONMarshallingError)
}
err = json.Unmarshal(jsonUploadOptions, &gcpUploadOptions)
if err != nil {
return HTTPError(ErrorJSONUnMarshallingError)
}
var share []string
if gcpUploadOptions.ShareWithAccounts != nil {
share = *gcpUploadOptions.ShareWithAccounts
}
var region string
if gcpUploadOptions.Region != nil {
region = *gcpUploadOptions.Region
}
object := fmt.Sprintf("composer-api-%s", uuid.New().String())
t := target.NewGCPTarget(&target.GCPTargetOptions{
Filename: imageType.Filename(),
Region: region,
Os: "", // not exposed in cloudapi for now
Bucket: gcpUploadOptions.Bucket,
Object: object,
ShareWithAccounts: share,
})
// Import will fail if an image with this name already exists
if gcpUploadOptions.ImageName != nil {
t.ImageName = *gcpUploadOptions.ImageName
} else {
t.ImageName = object
}
targets = append(targets, t)
} else if uploadRequest.Type == UploadTypes_azure {
var azureUploadOptions AzureUploadRequestOptions
jsonUploadOptions, err := json.Marshal(uploadRequest.Options)
if err != nil {
return HTTPError(ErrorJSONMarshallingError)
}
err = json.Unmarshal(jsonUploadOptions, &azureUploadOptions)
if err != nil {
return HTTPError(ErrorJSONUnMarshallingError)
}
t := target.NewAzureImageTarget(&target.AzureImageTargetOptions{
Filename: imageType.Filename(),
TenantID: azureUploadOptions.TenantId,
Location: azureUploadOptions.Location,
SubscriptionID: azureUploadOptions.SubscriptionId,
ResourceGroup: azureUploadOptions.ResourceGroup,
})
if azureUploadOptions.ImageName != nil {
t.ImageName = *azureUploadOptions.ImageName
} else {
// if ImageName wasn't given, generate a random one
t.ImageName = fmt.Sprintf("composer-api-%s", uuid.New().String())
}
targets = append(targets, t)
} else {
return HTTPError(ErrorInvalidUploadType)
}
}
var ir imageRequest
if len(imageRequests) == 1 {
// NOTE: the store currently does not support multi-image composes
ir = imageRequests[0]
} else {
return HTTPError(ErrorMultiImageCompose)
}
id, err := h.server.workers.EnqueueOSBuild(ir.arch, &worker.OSBuildJob{
Manifest: ir.manifest,
Targets: targets,
Exports: ir.exports,
})
if err != nil {
return HTTPError(ErrorEnqueueingJob)
}
return ctx.JSON(http.StatusCreated, &ComposeId{
ObjectReference: ObjectReference{
Href: "/api/composer/v2/compose",
Id: id.String(),
Kind: "ComposeId",
},
Id: id.String(),
})
}
func (h *apiHandlers) GetComposeStatus(ctx echo.Context, id string) error {
jobId, err := uuid.Parse(id)
if err != nil {
return HTTPError(ErrorInvalidComposeId)
}
var result worker.OSBuildJobResult
status, _, err := h.server.workers.JobStatus(jobId, &result)
if err != nil {
return HTTPError(ErrorComposeNotFound)
}
var us *UploadStatus
if result.TargetResults != nil {
// Only single upload target is allowed, therefore only a single upload target result is allowed as well
if len(result.TargetResults) != 1 {
return HTTPError(ErrorSeveralUploadTargets)
}
tr := *result.TargetResults[0]
var uploadType UploadTypes
var uploadOptions interface{}
switch tr.Name {
case "org.osbuild.aws":
uploadType = UploadTypes_aws
awsOptions := tr.Options.(*target.AWSTargetResultOptions)
uploadOptions = AWSUploadStatus{
Ami: awsOptions.Ami,
Region: awsOptions.Region,
}
case "org.osbuild.aws.s3":
uploadType = UploadTypes_aws_s3
awsOptions := tr.Options.(*target.AWSS3TargetResultOptions)
uploadOptions = AWSS3UploadStatus{
Url: awsOptions.URL,
}
case "org.osbuild.gcp":
uploadType = UploadTypes_gcp
gcpOptions := tr.Options.(*target.GCPTargetResultOptions)
uploadOptions = GCPUploadStatus{
ImageName: gcpOptions.ImageName,
ProjectId: gcpOptions.ProjectID,
}
case "org.osbuild.azure.image":
uploadType = UploadTypes_azure
gcpOptions := tr.Options.(*target.AzureImageTargetResultOptions)
uploadOptions = AzureUploadStatus{
ImageName: gcpOptions.ImageName,
}
default:
return HTTPError(ErrorUnknownUploadTarget)
}
us = &UploadStatus{
Status: result.UploadStatus,
Type: uploadType,
Options: uploadOptions,
}
}
return ctx.JSON(http.StatusOK, ComposeStatus{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/composer/v2/compose/%v", jobId),
Id: jobId.String(),
Kind: "ComposeStatus",
},
ImageStatus: ImageStatus{
Status: composeStatusFromJobStatus(status, &result),
UploadStatus: us,
},
})
}
func composeStatusFromJobStatus(js *worker.JobStatus, result *worker.OSBuildJobResult) ImageStatusValue {
if js.Canceled {
return ImageStatusValue_failure
}
if js.Started.IsZero() {
return ImageStatusValue_pending
}
if js.Finished.IsZero() {
// TODO: handle also ImageStatusValue_uploading
// TODO: handle also ImageStatusValue_registering
return ImageStatusValue_building
}
if result.Success {
return ImageStatusValue_success
}
return ImageStatusValue_failure
}
// ComposeMetadata handles a /compose/{id}/metadata GET request
func (h *apiHandlers) GetComposeMetadata(ctx echo.Context, id string) error {
jobId, err := uuid.Parse(id)
if err != nil {
return HTTPError(ErrorInvalidComposeId)
}
var result worker.OSBuildJobResult
status, _, err := h.server.workers.JobStatus(jobId, &result)
if err != nil {
return HTTPError(ErrorComposeNotFound)
}
var job worker.OSBuildJob
if _, _, _, err = h.server.workers.Job(jobId, &job); err != nil {
return HTTPError(ErrorComposeNotFound)
}
if status.Finished.IsZero() {
// job still running: empty response
return ctx.JSON(200, ComposeMetadata{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/composer/v2/%v/metadata", jobId),
Id: jobId.String(),
Kind: "ComposeMetadata",
},
})
}
if status.Canceled || !result.Success {
// job canceled or failed, empty response
return ctx.JSON(200, ComposeMetadata{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/composer/v2/%v/metadata", jobId),
Id: jobId.String(),
Kind: "ComposeMetadata",
},
})
}
manifestVer, err := job.Manifest.Version()
if err != nil {
return HTTPError(ErrorFailedToParseManifestVersion)
}
if result.OSBuildOutput == nil || result.OSBuildOutput.Assembler == nil {
return HTTPError(ErrorMalformedOSBuildJobResult)
}
var rpms []rpmmd.RPM
var ostreeCommitResult *osbuild1.StageResult
var coreStages []osbuild1.StageResult
switch manifestVer {
case "1":
coreStages = result.OSBuildOutput.Stages
if assemblerResult := result.OSBuildOutput.Assembler; assemblerResult.Name == "org.osbuild.ostree.commit" {
ostreeCommitResult = result.OSBuildOutput.Assembler
}
case "2":
// v2 manifest results store all stage output in the main stages
// here we filter out the build stages to collect only the RPMs for the
// core stages
// the filtering relies on two assumptions:
// 1. the build pipeline is named "build"
// 2. the stage results from v2 manifests when converted to v1 are
// named by prefixing the pipeline name
for _, stage := range result.OSBuildOutput.Stages {
if !strings.HasPrefix(stage.Name, "build") {
coreStages = append(coreStages, stage)
}
}
// find the ostree.commit stage
for idx, stage := range result.OSBuildOutput.Stages {
if strings.HasSuffix(stage.Name, "org.osbuild.ostree.commit") {
ostreeCommitResult = &result.OSBuildOutput.Stages[idx]
break
}
}
default:
return HTTPError(ErrorUnknownManifestVersion)
}
rpms = rpmmd.OSBuildStagesToRPMs(coreStages)
packages := make([]PackageMetadata, len(rpms))
for idx, rpm := range rpms {
packages[idx] = PackageMetadata{
Type: rpm.Type,
Name: rpm.Name,
Version: rpm.Version,
Release: rpm.Release,
Epoch: rpm.Epoch,
Arch: rpm.Arch,
Sigmd5: rpm.Sigmd5,
Signature: rpm.Signature,
}
}
resp := &ComposeMetadata{
ObjectReference: ObjectReference{
Href: fmt.Sprintf("/api/composer/v2/compose/%v/metadata", jobId),
Id: jobId.String(),
Kind: "ComposeMetadata",
},
Packages: &packages,
}
if ostreeCommitResult != nil && ostreeCommitResult.Metadata != nil {
commitMetadata, ok := ostreeCommitResult.Metadata.(*osbuild1.OSTreeCommitStageMetadata)
if !ok {
return HTTPError(ErrorUnableToConvertOSTreeCommitStageMetadata)
}
resp.OstreeCommit = &commitMetadata.Compose.OSTreeCommit
}
return ctx.JSON(200, resp)
}

View file

@ -0,0 +1,417 @@
package v2_test
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/osbuild/osbuild-composer/internal/cloudapi/v2"
"github.com/osbuild/osbuild-composer/internal/distro/test_distro"
distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
"github.com/osbuild/osbuild-composer/internal/test"
"github.com/osbuild/osbuild-composer/internal/worker"
)
func newV2Server(t *testing.T, dir string) (*v2.Server, *worker.Server) {
rpmFixture := rpmmd_mock.BaseFixture(dir)
rpm := rpmmd_mock.NewRPMMDMock(rpmFixture)
require.NotNil(t, rpm)
distros, err := distro_mock.NewDefaultRegistry()
require.NoError(t, err)
require.NotNil(t, distros)
v2Server := v2.NewServer(log.New(os.Stdout, "", 0), rpmFixture.Workers, rpm, distros)
require.NotNil(t, v2Server)
return v2Server, rpmFixture.Workers
}
func TestUnknownRoute(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, _ := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", "/api/composer/v2/badroute", ``, http.StatusNotFound, `
{
"href": "/api/composer/v2/errors/21",
"id": "21",
"kind": "Error",
"code": "COMPOSER-21",
"reason": "Requested resource doesn't exist"
}`, "operation_id")
}
func TestGetError(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, _ := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", "/api/composer/v2/errors/4", ``, http.StatusOK, `
{
"href": "/api/composer/v2/errors/4",
"id": "4",
"kind": "Error",
"code": "COMPOSER-4",
"reason": "Unsupported distribution"
}`, "operation_id")
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", "/api/composer/v2/errors/3000", ``, http.StatusNotFound, `
{
"href": "/api/composer/v2/errors/17",
"id": "17",
"kind": "Error",
"code": "COMPOSER-17",
"reason": "Error with given id not found"
}`, "operation_id")
}
func TestGetErrorList(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, _ := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", "/api/composer/v2/errors?page=3&size=1", ``, http.StatusOK, `
{
"kind": "ErrorList",
"page": 3,
"size": 1,
"items": [{
"href": "/api/composer/v2/errors/4",
"id": "4",
"kind": "Error",
"code": "COMPOSER-4",
"reason": "Unsupported distribution"
}]
}`, "operation_id", "total")
}
func TestCompose(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, _ := newV2Server(t, dir)
// unsupported distribution
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "unsupported_distro",
"image_requests":[{
"architecture": "%s",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusBadRequest, `
{
"href": "/api/composer/v2/errors/4",
"id": "4",
"kind": "Error",
"code": "COMPOSER-4",
"reason": "Unsupported distribution"
}`, "operation_id")
// unsupported architecture
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_requests":[{
"architecture": "unsupported_arch",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestImageTypeName), http.StatusBadRequest, `
{
"href": "/api/composer/v2/errors/5",
"id": "5",
"kind": "Error",
"code": "COMPOSER-5",
"reason": "Unsupported architecture"
}`, "operation_id")
// unsupported imagetype
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_requests":[{
"architecture": "%s",
"image_type": "unsupported_image_type",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestArchName), http.StatusBadRequest, `
{
"href": "/api/composer/v2/errors/6",
"id": "6",
"kind": "Error",
"code": "COMPOSER-6",
"reason": "Unsupported image type"
}`, "operation_id")
// Returns 404, but should be 405; see https://github.com/labstack/echo/issues/1981
// test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", "/api/composer/v2/compose", fmt.Sprintf(`
// {
// "distribution": "%s",
// "image_requests":[{
// "architecture": "%s",
// "image_type": "%s",
// "repositories": [{
// "baseurl": "somerepo.org",
// "rhsm": false
// }],
// "upload_request": {
// "type": "aws.s3",
// "options": {
// "access_key_id": "somekey",
// "secret_access_key": "somesecretkey",
// "bucket": "somebucket"
// }
// }
// }]
// }`, test_distro.TestDistroName, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusMethodNotAllowed, `
// {
// "href": "/api/composer/v2/errors/22",
// "id": "22",
// "kind": "Error",
// "code": "COMPOSER-22",
// "reason": "Requested method isn't supported for resource"
// }`, "operation_id")
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_requests":[{
"architecture": "%s",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusCreated, `
{
"href": "/api/composer/v2/compose",
"kind": "ComposeId"
}`, "id")
}
func TestComposeStatusSuccess(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, wrksrv := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_requests":[{
"architecture": "%s",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusCreated, `
{
"href": "/api/composer/v2/compose",
"kind": "ComposeId"
}`, "id")
jobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArchName, []string{"osbuild"})
require.NoError(t, err)
require.Equal(t, "osbuild", jobType)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", fmt.Sprintf("/api/composer/v2/compose/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/composer/v2/compose/%v",
"kind": "ComposeStatus",
"id": "%v",
"image_status": {"status": "building"}
}`, jobId, jobId))
// todo make it an osbuildjobresult
res, err := json.Marshal(&worker.OSBuildJobResult{
Success: true,
})
require.NoError(t, err)
err = wrksrv.FinishJob(token, res)
require.NoError(t, err)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", fmt.Sprintf("/api/composer/v2/compose/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/composer/v2/compose/%v",
"kind": "ComposeStatus",
"id": "%v",
"image_status": {"status": "success"}
}`, jobId, jobId))
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", fmt.Sprintf("/api/composer/v2/compose/%v/metadata", jobId), ``, http.StatusInternalServerError, `
{
"href": "/api/composer/v2/errors/1012",
"id": "1012",
"kind": "Error",
"code": "COMPOSER-1012",
"reason": "OSBuildJobResult does not have expected fields set"
}`, "operation_id")
}
func TestComposeStatusFailure(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, wrksrv := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"image_requests":[{
"architecture": "%s",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusCreated, `
{
"href": "/api/composer/v2/compose",
"kind": "ComposeId"
}`, "id")
jobId, token, jobType, _, _, err := wrksrv.RequestJob(context.Background(), test_distro.TestArchName, []string{"osbuild"})
require.NoError(t, err)
require.Equal(t, "osbuild", jobType)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", fmt.Sprintf("/api/composer/v2/compose/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/composer/v2/compose/%v",
"kind": "ComposeStatus",
"id": "%v",
"image_status": {"status": "building"}
}`, jobId, jobId))
err = wrksrv.FinishJob(token, nil)
require.NoError(t, err)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "GET", fmt.Sprintf("/api/composer/v2/compose/%v", jobId), ``, http.StatusOK, fmt.Sprintf(`
{
"href": "/api/composer/v2/compose/%v",
"kind": "ComposeStatus",
"id": "%v",
"image_status": {"status": "failure"}
}`, jobId, jobId))
}
func TestComposeCustomizations(t *testing.T) {
dir, err := ioutil.TempDir("", "osbuild-composer-test-api-v2-")
require.NoError(t, err)
defer os.RemoveAll(dir)
srv, _ := newV2Server(t, dir)
test.TestRoute(t, srv.Handler("/api/composer/v2"), false, "POST", "/api/composer/v2/compose", fmt.Sprintf(`
{
"distribution": "%s",
"customizations": {
"subscription": {
"organization": "2040324",
"activation_key": "my-secret-key",
"server_url": "subscription.rhsm.redhat.com",
"base_url": "http://cdn.redhat.com/",
"insights": true
},
"packages": [ "pkg1", "pkg2" ],
"users": [{
"name": "user1",
"groups": [ "wheel" ],
"key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrGKErMYi+MMUwuHaRAJmRLoIzRf2qD2dD5z0BTx/6x"
}]
},
"image_requests":[{
"architecture": "%s",
"image_type": "%s",
"repositories": [{
"baseurl": "somerepo.org",
"rhsm": false
}],
"upload_request": {
"type": "aws.s3",
"options": {
"access_key_id": "somekey",
"secret_access_key": "somesecretkey",
"bucket": "somebucket"
}
}
}]
}`, test_distro.TestDistroName, test_distro.TestArchName, test_distro.TestImageTypeName), http.StatusCreated, `
{
"href": "/api/composer/v2/compose",
"kind": "ComposeId"
}`, "id")
}