Skip to content

Commit

Permalink
Enable Bicep recipe unit-test with fake registry server (#7021)
Browse files Browse the repository at this point in the history
# Description

This is to enable bicep recipe unit test with the fake registry server.

## Type of change

<!--

Please select **one** of the following options that describes your
change and delete the others. Clearly identifying the type of change you
are making will help us review your PR faster, and is used in authoring
release notes.

If you are making a bug fix or functionality change to Radius and do not
have an associated issue link please create one now.

-->

- This pull request fixes a bug in Radius and has an approved issue
(issue link required).

<!--

Please update the following to link the associated issue. This is
required for some kinds of changes (see above).

-->

Fixes: #6490

---------

Signed-off-by: Young Bu Park <youngp@microsoft.com>
  • Loading branch information
youngbupark authored Jan 12, 2024
1 parent be2bc0c commit d9d8efe
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 24 deletions.
18 changes: 11 additions & 7 deletions pkg/recipes/driver/bicep.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/radius-project/radius/pkg/to"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2/registry/remote"

"github.com/go-logr/logr"
coredm "github.com/radius-project/radius/pkg/corerp/datamodel"
"github.com/radius-project/radius/pkg/metrics"
"github.com/radius-project/radius/pkg/portableresources/datamodel"
"github.com/radius-project/radius/pkg/portableresources/processors"
Expand All @@ -37,12 +38,11 @@ import (
recipes_util "github.com/radius-project/radius/pkg/recipes/util"
"github.com/radius-project/radius/pkg/rp/util"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
clients "github.com/radius-project/radius/pkg/sdk/clients"
"github.com/radius-project/radius/pkg/sdk/clients"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/pkg/ucp/resources"
resources_radius "github.com/radius-project/radius/pkg/ucp/resources/radius"
"github.com/radius-project/radius/pkg/ucp/ucplog"

coredm "github.com/radius-project/radius/pkg/corerp/datamodel"
)

//go:generate mockgen -destination=./mock_driver.go -package=driver -self_package github.com/radius-project/radius/pkg/recipes/driver github.com/radius-project/radius/pkg/recipes/driver Driver
Expand Down Expand Up @@ -74,6 +74,9 @@ type bicepDriver struct {
DeploymentClient *clients.ResourceDeploymentsClient
ResourceClient processors.ResourceClient
options BicepOptions

// RegistryClient is the optional client used to interact with the container registry.
RegistryClient remote.Client
}

// Execute fetches recipe contents from container registry, creates a deployment ID, a recipe context parameter, recipe parameters,
Expand All @@ -85,7 +88,8 @@ func (d *bicepDriver) Execute(ctx context.Context, opts ExecuteOptions) (*recipe

recipeData := make(map[string]any)
downloadStartTime := time.Now()
err := util.ReadFromRegistry(ctx, opts.Definition, &recipeData)

err := util.ReadFromRegistry(ctx, opts.Definition, &recipeData, d.RegistryClient)
if err != nil {
metrics.DefaultRecipeEngineMetrics.RecordRecipeDownloadDuration(ctx, downloadStartTime,
metrics.NewRecipeAttributes(metrics.RecipeEngineOperationDownloadRecipe, opts.Recipe.Name, &opts.Definition, recipes.RecipeDownloadFailed))
Expand Down Expand Up @@ -258,7 +262,7 @@ func (d *bicepDriver) GetRecipeMetadata(ctx context.Context, opts BaseOptions) (
// }
// }
recipeData := make(map[string]any)
err := util.ReadFromRegistry(ctx, opts.Definition, &recipeData)
err := util.ReadFromRegistry(ctx, opts.Definition, &recipeData, d.RegistryClient)
if err != nil {
return nil, err
}
Expand Down
39 changes: 23 additions & 16 deletions pkg/recipes/driver/bicep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@ package driver

import (
"fmt"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
gomock "github.com/golang/mock/gomock"
v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
corerp_datamodel "github.com/radius-project/radius/pkg/corerp/datamodel"
"github.com/radius-project/radius/pkg/portableresources/processors"
"github.com/radius-project/radius/pkg/recipes"
"github.com/radius-project/radius/pkg/recipes/recipecontext"
"github.com/radius-project/radius/pkg/rp/util/registrytest"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
clients "github.com/radius-project/radius/pkg/sdk/clients"
"github.com/radius-project/radius/pkg/to"
"github.com/radius-project/radius/pkg/ucp/resources"
resources_kubernetes "github.com/radius-project/radius/pkg/ucp/resources/kubernetes"
"github.com/radius-project/radius/test/testcontext"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -376,7 +379,9 @@ func Test_Bicep_PrepareRecipeResponse_EmptyResult(t *testing.T) {
}

func Test_Bicep_Execute_SimulatedEnvironment(t *testing.T) {
t.Skip("This test makes outbound calls. #6490")
ts := registrytest.NewFakeRegistryServer(t)
t.Cleanup(ts.CloseServer)

opts := ExecuteOptions{
BaseOptions: BaseOptions{
Configuration: recipes.Configuration{
Expand All @@ -395,13 +400,13 @@ func Test_Bicep_Execute_SimulatedEnvironment(t *testing.T) {
Definition: recipes.EnvironmentDefinition{
Name: "test-recipe",
Driver: recipes.TemplateKindBicep,
TemplatePath: "ghcr.io/radius-project/dev/recipes/functionaltest/parameters/mongodatabases/azure:1.0",
TemplatePath: ts.TestImageURL,
ResourceType: "Applications.Datastores/mongoDatabases",
},
},
}
ctx := testcontext.New(t)
d := &bicepDriver{}
d := &bicepDriver{RegistryClient: ts.TestServer.Client()}
recipesOutput, err := d.Execute(ctx, opts)
require.NoError(t, err)
require.Nil(t, recipesOutput)
Expand Down Expand Up @@ -489,20 +494,21 @@ func Test_Bicep_Delete_Error(t *testing.T) {
}

func Test_Bicep_GetRecipeMetadata_Success(t *testing.T) {
t.Skip("This test makes outbound calls. #6490")
ts := registrytest.NewFakeRegistryServer(t)
t.Cleanup(ts.CloseServer)

ctx := testcontext.New(t)
driver := bicepDriver{}
driver := &bicepDriver{RegistryClient: ts.TestServer.Client()}
recipeDefinition := recipes.EnvironmentDefinition{
Name: "mongo-azure",
Driver: recipes.TemplateKindBicep,
TemplatePath: "ghcr.io/radius-project/dev/recipes/functionaltest/parameters/mongodatabases/azure:1.0",
TemplatePath: ts.TestImageURL,
ResourceType: "Applications.Datastores/mongoDatabases",
}

expectedOutput := map[string]any{
"documentdbName": map[string]any{"type": "string"},
"location": map[string]any{"defaultValue": "[resourceGroup().location]", "type": "string"},
"mongodbName": map[string]any{"type": "string"},
}

recipeData, err := driver.GetRecipeMetadata(ctx, BaseOptions{
Expand All @@ -515,30 +521,31 @@ func Test_Bicep_GetRecipeMetadata_Success(t *testing.T) {
}

func Test_Bicep_GetRecipeMetadata_Error(t *testing.T) {
t.Skip("This test makes outbound calls. #6490")
ts := registrytest.NewFakeRegistryServer(t)
t.Cleanup(ts.CloseServer)

ctx := testcontext.New(t)
driver := bicepDriver{}
driver := &bicepDriver{RegistryClient: ts.TestServer.Client()}
recipeDefinition := recipes.EnvironmentDefinition{
Name: "mongo-azure",
Driver: recipes.TemplateKindBicep,
TemplatePath: "ghcr.io/radius-project/dev/test-non-existent-recipe",
TemplatePath: ts.TestServer.URL + "/nonexisting:latest",
ResourceType: "Applications.Datastores/mongoDatabases",
}

_, err := driver.GetRecipeMetadata(ctx, BaseOptions{
_, actualErr := driver.GetRecipeMetadata(ctx, BaseOptions{
Recipe: recipes.ResourceMetadata{},
Definition: recipeDefinition,
})
expErr := recipes.RecipeError{
ErrorDetails: v1.ErrorDetails{
Code: recipes.RecipeLanguageFailure,
Message: "failed to fetch repository from the path \"ghcr.io/radius-project/dev/test-non-existent-recipe\": ghcr.io/radius-project/dev/test-non-existent-recipe:latest: not found",
Message: "failed to fetch repository from the path \"https://<REPLACE_HOST>/nonexisting:latest\": <REPLACE_HOST>/nonexisting:latest: not found",
},
DeploymentStatus: "setupError",
}

require.Error(t, err)
require.Equal(t, err, &expErr)
expErr.ErrorDetails.Message = strings.Replace(expErr.ErrorDetails.Message, "<REPLACE_HOST>", ts.URL.Host, -1)
require.Equal(t, actualErr, &expErr)
}

func Test_GetGCOutputResources(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion pkg/rp/util/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
// ReadFromRegistry reads data from an OCI compliant registry and stores it in a map. It returns an error if the path is invalid,
// if the client to the registry fails to be created, if the manifest fails to be fetched, if the bytes fail to be fetched, or if
// the data fails to be unmarshalled.
func ReadFromRegistry(ctx context.Context, definition recipes.EnvironmentDefinition, data *map[string]any) error {
func ReadFromRegistry(ctx context.Context, definition recipes.EnvironmentDefinition, data *map[string]any, client remote.Client) error {
registryRepo, tag, err := parsePath(definition.TemplatePath)
if err != nil {
return v1.NewClientErrInvalidRequest(fmt.Sprintf("invalid path %s", err.Error()))
Expand All @@ -42,6 +42,7 @@ func ReadFromRegistry(ctx context.Context, definition recipes.EnvironmentDefinit
if err != nil {
return fmt.Errorf("failed to create client to registry %s", err.Error())
}
repo.Client = client

if definition.PlainHTTP {
repo.PlainHTTP = true
Expand Down Expand Up @@ -88,6 +89,7 @@ func getDigestFromManifest(ctx context.Context, repo *remote.Repository, tag str
if err != nil {
return "", err
}

// get the layers digest to fetch the blob
layer, ok := manifest["layers"].([]any)[0].(map[string]any)
if !ok {
Expand Down
120 changes: 120 additions & 0 deletions pkg/rp/util/registrytest/fakeregistryserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright 2023 The Radius Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package registrytest

import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"

"github.com/go-chi/chi/v5"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

type fakeServerInfo struct {
TestServer *httptest.Server
URL *url.URL
CloseServer func()
TestImageURL string
ImageName string
}

// NewFakeRegistryServer creates a fake registry server that serves a single blob and index.
func NewFakeRegistryServer(t *testing.T) fakeServerInfo {
blob := []byte(`{
"parameters": {
"documentdbName": {
"type": "string"
},
"location": {
"defaultValue": "[resourceGroup().location]",
"type": "string"
}
}
}`)

blobDesc := ocispec.Descriptor{
MediaType: "recipe",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}

index := []byte(`{
"layers": [
{
"digest": "` + blobDesc.Digest.String() + `"
}
]
}`)

indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}

r := chi.NewRouter()
r.Route("/v2/test", func(r chi.Router) {
r.Head("/manifests/{ref}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size)))
w.WriteHeader(http.StatusOK)
})

r.Get("/manifests/"+indexDesc.Digest.String(), func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
})

r.Head("/blobs/{digest}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", blobDesc.MediaType)
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size)))
w.WriteHeader(http.StatusOK)
})

r.Get("/blobs/"+blobDesc.Digest.String(), func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
})
})

ts := httptest.NewTLSServer(r)

url, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse url: %v", err)
}

return fakeServerInfo{
TestServer: ts,
URL: url,
CloseServer: ts.Close,
TestImageURL: ts.URL + "/test:latest",
ImageName: "test:latest",
}
}

0 comments on commit d9d8efe

Please sign in to comment.