Skip to content

Commit

Permalink
feat: enable arm64images
Browse files Browse the repository at this point in the history
Verifies the image is built for the right architecture with image builder.

Refs HMS-2818
  • Loading branch information
ezr-ondrej committed Oct 20, 2023
1 parent f9724b4 commit 6105c98
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 67 deletions.
1 change: 1 addition & 0 deletions internal/clients/http/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
var (
ErrCloneNotFound = usrerr.New(404, "image clone not found", "")
ErrImageStatus = usrerr.New(500, "build of requested image has not finished yet", "image still building")
ErrImageArchInvalid = usrerr.New(400, "the image provided has invalid architecture and can not be launch with provided instance type", "invalid image architecture")
ErrUnknownImageType = usrerr.New(500, "unknown image type", "")
ErrUploadStatus = usrerr.New(500, "cannot get image status", "")
ErrImageRequestNotFound = usrerr.New(500, "image compose request not found", "")
Expand Down
93 changes: 50 additions & 43 deletions internal/clients/http/image_builder/image_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package image_builder

import (
"context"
"errors"
"fmt"

"github.com/RHEnVision/provisioning-backend/internal/clients"
Expand All @@ -26,7 +27,11 @@ func logger(ctx context.Context) zerolog.Logger {
}

func newImageBuilderClient(ctx context.Context) (clients.ImageBuilder, error) {
c, err := NewClientWithResponses(config.ImageBuilder.URL, func(c *Client) error {
return NewImageBuilderClientWithUrl(ctx, config.ImageBuilder.URL)
}

func NewImageBuilderClientWithUrl(ctx context.Context, url string) (clients.ImageBuilder, error) {
c, err := NewClientWithResponses(url, func(c *Client) error {
c.Client = http.NewPlatformClient(ctx, config.ImageBuilder.Proxy.URL)
return nil
})
Expand Down Expand Up @@ -58,14 +63,11 @@ func (c *ibClient) Ready(ctx context.Context) error {
return nil
}

func (c *ibClient) GetAWSAmi(ctx context.Context, composeID string) (string, error) {
func (c *ibClient) GetAWSAmi(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
logger := logger(ctx)
if _, err := uuid.Parse(composeID); err != nil {
return "", fmt.Errorf("compose ID '%s' is not valid UUID: %w", composeID, clients.ErrBadRequest)
}
logger.Trace().Str("compose_id", composeID).Msgf("Getting AMI of compose ID %v", composeID)
logger.Trace().Msgf("Getting AMI of compose ID %s", composeUUID.String())

imageStatus, err := c.fetchImageStatus(ctx, composeID)
imageStatus, err := c.fetchImageStatus(ctx, composeUUID, instanceType)
if err != nil {
return "", err
}
Expand All @@ -81,17 +83,16 @@ func (c *ibClient) GetAWSAmi(ctx context.Context, composeID string) (string, err
return "", fmt.Errorf("%w: not an AWS status", http.ErrUploadStatus)
}

logger.Info().Str("compose_id", composeID).Str("ami", uploadStatus.Ami).
Msgf("Translated compose ID %s to AMI %s", composeID, uploadStatus.Ami)
logger.Info().Msgf("Translated compose ID %s to AMI %s", composeUUID, uploadStatus.Ami)

return uploadStatus.Ami, nil
}

func (c *ibClient) GetAzureImageID(ctx context.Context, composeID string) (string, error) {
func (c *ibClient) GetAzureImageID(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
logger := logger(ctx)
logger.Trace().Msgf("Getting Azure ID of image %v", composeID)
logger.Trace().Msgf("Getting Azure ID of image %v", composeUUID.String())

composeStatus, err := c.getComposeStatus(ctx, composeID)
composeStatus, err := c.getComposeStatus(ctx, composeUUID)
if err != nil {
return "", err
}
Expand All @@ -112,6 +113,11 @@ func (c *ibClient) GetAzureImageID(ctx context.Context, composeID string) (strin
return "", http.ErrImageRequestNotFound
}

imageArch, archErr := clients.MapArchitectures(ctx, string(composeStatus.Request.ImageRequests[0].Architecture))
if archErr != nil || imageArch != instanceType.Architecture {
return "", http.ErrImageArchInvalid
}

uploadOptions, err := composeStatus.ImageStatus.UploadStatus.Options.AsAzureUploadStatus()
if err != nil {
return "", fmt.Errorf("%w: not an Azure status", http.ErrUploadStatus)
Expand All @@ -124,11 +130,11 @@ func (c *ibClient) GetAzureImageID(ctx context.Context, composeID string) (strin
return fmt.Sprintf("/resourceGroups/%s/providers/Microsoft.Compute/images/%s", azureUploadRequest.ResourceGroup, uploadOptions.ImageName), nil
}

func (c *ibClient) GetGCPImageName(ctx context.Context, composeID string) (string, error) {
func (c *ibClient) GetGCPImageName(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
logger := logger(ctx)
logger.Trace().Str("compose_id", composeID).Msgf("Getting Google image id of compose %s", composeID)
logger.Trace().Msgf("Getting Google image id of compose %s", composeUUID)

imageStatus, err := c.fetchImageStatus(ctx, composeID)
imageStatus, err := c.fetchImageStatus(ctx, composeUUID, instanceType)
if err != nil {
return "", err
}
Expand All @@ -146,37 +152,34 @@ func (c *ibClient) GetGCPImageName(ctx context.Context, composeID string) (strin
}

result := fmt.Sprintf("projects/%s/global/images/%s", uploadStatus.ProjectId, uploadStatus.ImageName)
logger.Info().Str("compose_id", composeID).Str("ami", result).
Msgf("Translated compose ID %s to AMI %s", composeID, result)
logger.Info().Msgf("Translated compose ID %s to image name %s", composeUUID, result)

return result, nil
}

func (c *ibClient) fetchImageStatus(ctx context.Context, composeID string) (*UploadStatus, error) {
func (c *ibClient) fetchImageStatus(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (*UploadStatus, error) {
ctx, span := telemetry.StartSpan(ctx, "fetchImageStatus")
defer span.End()
logger := logger(ctx)
logger.Trace().Msgf("Fetching image status %v", composeID)
logger.Trace().Msgf("Fetching image status %v", composeUUID.String())

composeResp, err := c.checkCompose(ctx, composeID)
uploadStatus, err := c.checkCompose(ctx, composeUUID, instanceType)
if err != nil {
cloneResp, err := c.checkClone(ctx, composeID)
if err != nil {
return nil, fmt.Errorf("could not find image neither in compose nor in clones: %w", err)
if errors.Is(err, clients.ErrUnexpectedBackendResponse) {
uploadStatus, err = c.checkClone(ctx, composeUUID)
if err != nil {
return nil, fmt.Errorf("could not find image neither in compose nor in clones: %w", err)
}
} else {
return nil, fmt.Errorf("image compose is not launchable: %w", err)
}
return cloneResp, nil
}
return composeResp, nil
return uploadStatus, nil
}

func (c *ibClient) getComposeStatus(ctx context.Context, composeID string) (*ComposeStatus, error) {
func (c *ibClient) getComposeStatus(ctx context.Context, composeUUID uuid.UUID) (*ComposeStatus, error) {
logger := logger(ctx)

composeUUID, err := uuid.Parse(composeID)
if err != nil {
return nil, fmt.Errorf("unable to parse UUID: %w", err)
}

resp, err := c.client.GetComposeStatusWithResponse(ctx, composeUUID, headers.AddImageBuilderIdentityHeader, headers.AddEdgeRequestIdHeader)
if err != nil {
logger.Warn().Err(err).Msg("Failed to fetch image status from image builder")
Expand All @@ -194,11 +197,13 @@ func (c *ibClient) getComposeStatus(ctx context.Context, composeID string) (*Com
return resp.JSON200, nil
}

func (c *ibClient) checkCompose(ctx context.Context, composeID string) (*UploadStatus, error) {
// checkCompose validates whether the image identified by composeUUID is already successfully built.
// It also checks whether the target instance type architecture matches the image architecture.
func (c *ibClient) checkCompose(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (*UploadStatus, error) {
logger := logger(ctx)
logger.Trace().Msgf("Fetching image status %v from composes", composeID)
logger.Trace().Msgf("Fetching image status %v from composes", composeUUID)

composeStatus, err := c.getComposeStatus(ctx, composeID)
composeStatus, err := c.getComposeStatus(ctx, composeUUID)
if err != nil {
return nil, err
}
Expand All @@ -208,17 +213,19 @@ func (c *ibClient) checkCompose(ctx context.Context, composeID string) (*UploadS
return nil, http.ErrImageStatus
}

if len(composeStatus.Request.ImageRequests) == 1 {
imageArch, archErr := clients.MapArchitectures(ctx, string(composeStatus.Request.ImageRequests[0].Architecture))
if archErr != nil || imageArch != instanceType.Architecture {
return nil, http.ErrImageArchInvalid
}
}

return composeStatus.ImageStatus.UploadStatus, nil
}

func (c *ibClient) checkClone(ctx context.Context, composeID string) (*UploadStatus, error) {
func (c *ibClient) checkClone(ctx context.Context, composeUUID uuid.UUID) (*UploadStatus, error) {
logger := logger(ctx)
logger.Trace().Msgf("Fetching image status %v from clones", composeID)

composeUUID, err := uuid.Parse(composeID)
if err != nil {
return nil, fmt.Errorf("unable to parse UUID: %w", err)
}
logger.Trace().Msgf("Fetching image status %v from clones", composeUUID)

resp, err := c.client.GetCloneStatusWithResponse(ctx, composeUUID, headers.AddImageBuilderIdentityHeader, headers.AddEdgeRequestIdHeader)
if err != nil {
Expand All @@ -234,8 +241,8 @@ func (c *ibClient) checkClone(ctx context.Context, composeID string) (*UploadSta
return nil, fmt.Errorf("fetch image status call: %w", clients.ErrUnexpectedBackendResponse)
}

if ImageStatusStatus(resp.JSON200.Status) != ImageStatusStatusSuccess {
logger.Warn().Msg("Clone status is not ready")
if resp.JSON200.Status != UploadStatusStatusSuccess {
logger.Warn().Msgf("Clone status (%s) is not ready", resp.JSON200.Status)
return nil, http.ErrImageStatus
}

Expand Down
91 changes: 91 additions & 0 deletions internal/clients/http/image_builder/image_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package image_builder_test

import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

httpClients "github.com/RHEnVision/provisioning-backend/internal/clients/http"
"github.com/RHEnVision/provisioning-backend/internal/clients/http/image_builder"
"github.com/RHEnVision/provisioning-backend/internal/preload"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func composeStatusServer(t *testing.T) *httptest.Server {
t.Helper()

uploadOptions := &image_builder.UploadStatus_Options{}
err := uploadOptions.FromAWSUploadStatus(image_builder.AWSUploadStatus{
Ami: "ami-1234-test",
Region: "us-east-1",
})
require.NoError(t, err)
response := image_builder.ComposeStatus{
ImageStatus: image_builder.ImageStatus{
Status: image_builder.ImageStatusStatusSuccess,
UploadStatus: &image_builder.UploadStatus{
Status: image_builder.UploadStatusStatusSuccess,
Type: image_builder.UploadTypesAws,
Options: *uploadOptions,
},
},
Request: image_builder.ComposeRequest{
Distribution: image_builder.Rhel9,
ImageRequests: []image_builder.ImageRequest{{
Architecture: image_builder.ImageRequestArchitectureAarch64,
}},
},
}

return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
responseString, marshalErr := json.Marshal(response)
require.NoError(t, marshalErr)
_, err := io.WriteString(w, string(responseString))
require.NoError(t, err, "failed to write http body for stubbed server")
}))
}

func Test_GetAWSAmi(t *testing.T) {
t.Run("fails to resolve AMI for mismatching architecture image", func(t *testing.T) {
composeUUID, err := uuid.NewRandom()
require.NoError(t, err)
instanceType := preload.EC2InstanceType.FindInstanceType("t3.nano")
require.NotNil(t, instanceType, "failed to find instance type")

ts := composeStatusServer(t)
defer ts.Close()

ctx := context.Background()
client, err := image_builder.NewImageBuilderClientWithUrl(ctx, ts.URL)
require.NoError(t, err, "failed to initialize sources client with test server")

_, amiErr := client.GetAWSAmi(ctx, composeUUID, *instanceType)

assert.ErrorIs(t, amiErr, httpClients.ErrImageArchInvalid, "Expected an architecture mismatch")
})

t.Run("resolves AMI correctly for matching image and instance architecture", func(t *testing.T) {
composeUUID, err := uuid.NewRandom()
require.NoError(t, err)
instanceType := preload.EC2InstanceType.FindInstanceType("t4g.nano")
require.NotNil(t, instanceType, "failed to find instance type")

ts := composeStatusServer(t)
defer ts.Close()

ctx := context.Background()
client, err := image_builder.NewImageBuilderClientWithUrl(ctx, ts.URL)
require.NoError(t, err, "failed to initialize sources client with test server")

ami, amiErr := client.GetAWSAmi(ctx, composeUUID, *instanceType)
assert.NoError(t, amiErr, "expected to resolve AMI correctly")
assert.Equal(t, "ami-1234-test", ami)
})
}
10 changes: 7 additions & 3 deletions internal/clients/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/RHEnVision/provisioning-backend/internal/models"
"github.com/google/uuid"
)

// GetSourcesClient returns Sources interface implementation. There are currently
Expand Down Expand Up @@ -35,15 +36,18 @@ var GetImageBuilderClient func(ctx context.Context) (ImageBuilder, error)
// ImageBuilder interface provides access to the Image Builder backend service API
type ImageBuilder interface {
// GetAWSAmi returns related AWS image AMI identifier
GetAWSAmi(ctx context.Context, composeID string) (string, error)
// It also verifies the image is built successfully and for the right architecture.
GetAWSAmi(ctx context.Context, composeUUID uuid.UUID, instanceType InstanceType) (string, error)

// GetAzureImageID returns partial image id, that is missing the subscription prefix
// Full name is /subscriptions/<subscription-id>/resourceGroups/<Group>/providers/Microsoft.Compute/images/<ImageName>
// GetAzureImageID returns /resourceGroups/<Group>/providers/Microsoft.Compute/images/<ImageName>
GetAzureImageID(ctx context.Context, composeID string) (string, error)
// It also verifies the image is built successfully and for the right architecture.
GetAzureImageID(ctx context.Context, composeUUID uuid.UUID, instanceType InstanceType) (string, error)

// GetGCPImageName returns GCP image name
GetGCPImageName(ctx context.Context, composeID string) (string, error)
// It also verifies the image is built successfully and for the right architecture.
GetGCPImageName(ctx context.Context, composeUUID uuid.UUID, instanceType InstanceType) (string, error)

// Ready returns readiness information
Ready(ctx context.Context) error
Expand Down
7 changes: 4 additions & 3 deletions internal/clients/stubs/image_builder_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/RHEnVision/provisioning-backend/internal/clients"
"github.com/google/uuid"
)

type imageBuilderCtxKeyType string
Expand Down Expand Up @@ -33,14 +34,14 @@ func (*ImageBuilderClientStub) Ready(ctx context.Context) error {
return nil
}

func (mock *ImageBuilderClientStub) GetAWSAmi(ctx context.Context, composeID string) (string, error) {
func (mock *ImageBuilderClientStub) GetAWSAmi(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
return "ami-0c830793775595d4b-test", nil
}

func (mock *ImageBuilderClientStub) GetAzureImageID(ctx context.Context, composeID string) (string, error) {
func (mock *ImageBuilderClientStub) GetAzureImageID(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
return "/resourceGroups/redhat-deployed/providers/Microsoft.Compute/images/composer-api-92ea98f8-7697-472e-80b1-7454fa0e7fa7", nil
}

func (mock *ImageBuilderClientStub) GetGCPImageName(ctx context.Context, composeID string) (string, error) {
func (mock *ImageBuilderClientStub) GetGCPImageName(ctx context.Context, composeUUID uuid.UUID, instanceType clients.InstanceType) (string, error) {
return "projects/red-hat-image-builder/global/images/composer-api-871fa36d-0b5b-4001-8c95-a11f751a4d66-test", nil
}
Loading

0 comments on commit 6105c98

Please sign in to comment.