Skip to content

Commit

Permalink
Updates ingress configuration to loosely typed config object
Browse files Browse the repository at this point in the history
  • Loading branch information
wbreza committed Sep 3, 2024
1 parent f9c76f8 commit 3ee64ee
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 41 deletions.
33 changes: 33 additions & 0 deletions cli/azd/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,23 @@ type Config interface {
Raw() map[string]any
// similar to Raw() but it will resolve any vault references
ResolvedRaw() map[string]any
// Get retrieves the value stored at the specified path
Get(path string) (any, bool)
// GetString retrieves the value stored at the specified path as a string
GetString(path string) (string, bool)
// GetSection retrieves the value stored at the specified path and unmarshals it into the provided section
GetSection(path string, section any) (bool, error)
// GetMap retrieves the map stored at the specified path
GetMap(path string) (map[string]any, bool)
// GetSlice retrieves the slice stored at the specified path
GetSlice(path string) ([]any, bool)
// Set stores the value at the specified path
Set(path string, value any) error
// SetSecret stores the secrets at the specified path within a local user vault
SetSecret(path string, value string) error
// Unset removes the value stored at the specified path
Unset(path string) error
// IsEmpty returns a value indicating whether the configuration is empty
IsEmpty() bool
}

Expand Down Expand Up @@ -230,6 +241,28 @@ func (c *config) Get(path string) (any, bool) {
return nil, false
}

// GetMap retrieves the map stored at the specified path
func (c *config) GetMap(path string) (map[string]any, bool) {
value, ok := c.Get(path)
if !ok {
return nil, false
}

node, ok := value.(map[string]any)
return node, ok
}

// GetSlice retrieves the slice stored at the specified path
func (c *config) GetSlice(path string) ([]any, bool) {
value, ok := c.Get(path)
if !ok {
return nil, false
}

node, ok := value.([]any)
return node, ok
}

// Gets the value stored at the specified location as a string
func (c *config) GetString(path string) (string, bool) {
value, ok := c.Get(path)
Expand Down
60 changes: 19 additions & 41 deletions cli/azd/pkg/containerapps/container_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const (
pathTemplateContainers = "properties.template.containers"
pathConfigurationActiveRevisionsMode = "properties.configuration.activeRevisionsMode"
pathConfigurationSecrets = "properties.configuration.secrets"
pathConfigurationIngress = "properties.configuration.ingress"
pathConfigurationIngressTraffic = "properties.configuration.ingress.traffic"
pathConfigurationIngressFqdn = "properties.configuration.ingress.fqdn"
pathConfigurationIngressCustomDomains = "properties.configuration.ingress.customDomains"
Expand Down Expand Up @@ -149,40 +148,23 @@ func (cas *containerAppService) persistSettings(
log.Printf("failed getting current aca settings: %v. No settings will be persisted.", err)
}

var ingress *armappcontainers.Ingress
if has, err := aca.GetSection(pathConfigurationIngress, &ingress); !has || err != nil {
return obj, err
}
objConfig := config.NewConfig(obj)

if shouldPersistDomains &&
ingress.CustomDomains != nil {
acaAsConfig := config.NewConfig(obj)
customDomainsJson, err := convert.ToJsonArray(ingress.CustomDomains)
if err != nil {
return nil, fmt.Errorf("converting custom domains to JSON: %w", err)
if shouldPersistDomains {
customDomains, ok := aca.GetSlice(pathConfigurationIngressCustomDomains)
if ok {
objConfig.Set(pathConfigurationIngressCustomDomains, customDomains)

Check failure on line 156 in cli/azd/pkg/containerapps/container_app.go

View workflow job for this annotation

GitHub Actions / azd-lint (ubuntu-latest)

Error return value of `objConfig.Set` is not checked (errcheck)
}

if err := acaAsConfig.Set(pathConfigurationIngressCustomDomains, customDomainsJson); err != nil {
return nil, fmt.Errorf("failed to persist custom domains: %w", err)
}
obj = acaAsConfig.Raw()
}

if shouldPersistIngressSessionAffinity &&
ingress.StickySessions != nil {
acaAsConfig := config.NewConfig(obj)
stickySessionsJson, err := convert.ToJsonArray(ingress.StickySessions)
if err != nil {
return nil, fmt.Errorf("converting sticky sessions to JSON: %w", err)
}

if err := acaAsConfig.Set(pathConfigurationIngressStickySessions, stickySessionsJson); err != nil {
return nil, fmt.Errorf("failed to persist session affinity: %w", err)
if shouldPersistIngressSessionAffinity {
stickySessions, has := aca.Get(pathConfigurationIngressStickySessions)
if has {
objConfig.Set(pathConfigurationIngressStickySessions, stickySessions)

Check failure on line 163 in cli/azd/pkg/containerapps/container_app.go

View workflow job for this annotation

GitHub Actions / azd-lint (ubuntu-latest)

Error return value of `objConfig.Set` is not checked (errcheck)
}
obj = acaAsConfig.Raw()
}

return obj, nil
return objConfig.Raw(), nil
}

func (cas *containerAppService) DeployYaml(
Expand Down Expand Up @@ -322,7 +304,7 @@ func (cas *containerAppService) AddRevision(
}

var containers []map[string]any
if has, err = revision.GetSection(pathTemplateContainers, &containers); !has || err != nil {
if ok, err := revision.GetSection(pathTemplateContainers, &containers); !ok || err != nil {
return fmt.Errorf("getting containers: %w", err)
}

Expand All @@ -332,8 +314,8 @@ func (cas *containerAppService) AddRevision(
}

// Update the container app with the new revision
var revisionTemplate map[string]any
if has, err := revision.GetSection(pathTemplate, &revisionTemplate); !has || err != nil {
revisionTemplate, ok := revision.GetMap(pathTemplate)
if !ok {
return fmt.Errorf("getting revision template: %w", err)
}

Expand All @@ -352,15 +334,15 @@ func (cas *containerAppService) AddRevision(
return fmt.Errorf("updating container app revision: %w", err)
}

revisionMode, has := containerApp.GetString(pathConfigurationActiveRevisionsMode)
if !has {
revisionMode, ok := containerApp.GetString(pathConfigurationActiveRevisionsMode)
if !ok {
return fmt.Errorf("getting active revisions mode: %w", err)
}

// If the container app is in multiple revision mode, update the traffic to point to the new revision
if revisionMode == string(armappcontainers.ActiveRevisionsModeMultiple) {
revisionSuffix, has := revision.GetString(pathTemplateRevisionSuffix)
if !has {
revisionSuffix, ok := revision.GetString(pathTemplateRevisionSuffix)
if !ok {
return fmt.Errorf("getting revision suffix: %w", err)
}
newRevisionName := fmt.Sprintf("%s--%s", appName, revisionSuffix)
Expand All @@ -382,12 +364,8 @@ func (cas *containerAppService) syncSecrets(
containerApp config.Config,
) (config.Config, error) {
// If the container app doesn't have any existingSecrets, we don't need to do anything
var existingSecrets []any
if has, err := containerApp.GetSection(pathConfigurationSecrets, &existingSecrets); !has || err != nil {
return containerApp, err
}

if len(existingSecrets) == 0 {
existingSecrets, ok := containerApp.GetSlice(pathConfigurationSecrets)
if !ok || len(existingSecrets) == 0 {
return containerApp, nil
}

Expand Down
89 changes: 89 additions & 0 deletions cli/azd/pkg/containerapps/container_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,92 @@ func Test_ContainerApp_AddRevision(t *testing.T) {
require.Equal(t, updatedImageName, *updatedContainerApp.Properties.Template.Containers[0].Image)
require.Equal(t, "azd-0", *updatedContainerApp.Properties.Template.RevisionSuffix)
}

func Test_ContainerApp_DeployYaml(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())

subscriptionId := "SUBSCRIPTION_ID"
location := "eastus2"
resourceGroup := "RESOURCE_GROUP"
appName := "APP_NAME"

containerAppYaml := `
location: eastus2
name: APP_NAME
properties:
latestRevisionName: LATEST_REVISION_NAME
configuration:
activeRevisionsMode: Single
template:
containers:
- image: IMAGE_NAME
`

expected := &armappcontainers.ContainerApp{
Location: to.Ptr(location),
Name: to.Ptr(appName),
Properties: &armappcontainers.ContainerAppProperties{
LatestRevisionName: to.Ptr("LATEST_REVISION_NAME"),
Configuration: &armappcontainers.Configuration{
ActiveRevisionsMode: to.Ptr(armappcontainers.ActiveRevisionsModeSingle),
Ingress: &armappcontainers.Ingress{
CustomDomains: []*armappcontainers.CustomDomain{
{
Name: to.Ptr("DOMAIN_NAME"),
},
},
StickySessions: &armappcontainers.IngressStickySessions{
Affinity: to.Ptr(armappcontainers.AffinitySticky),
},
},
},
Template: &armappcontainers.Template{
Containers: []*armappcontainers.Container{
{
Image: to.Ptr("IMAGE_NAME"),
},
},
},
},
}

containerAppGetRequest := mockazsdk.MockContainerAppGet(
mockContext,
subscriptionId,
resourceGroup,
appName,
expected,
)
require.NotNil(t, containerAppGetRequest)

containerAppUpdateRequest := mockazsdk.MockContainerAppCreateOrUpdate(
mockContext,
subscriptionId,
resourceGroup,
appName,
expected,
)
require.NotNil(t, containerAppUpdateRequest)

cas := NewContainerAppService(
mockContext.SubscriptionCredentialProvider,
clock.NewMock(),
mockContext.ArmClientOptions,
mockContext.AlphaFeaturesManager,
)

err := mockContext.Config.Set("alpha.aca.persistDomains", "on")
require.NoError(t, err)
err = mockContext.Config.Set("alpha.aca.persistIngressSessionAffinity", "on")
require.NoError(t, err)

err = cas.DeployYaml(*mockContext.Context, subscriptionId, resourceGroup, appName, []byte(containerAppYaml), nil)
require.NoError(t, err)

var actual *armappcontainers.ContainerApp
err = mocks.ReadHttpBody(containerAppUpdateRequest.Body, &actual)
require.NoError(t, err)

require.Equal(t, expected.Properties.Configuration, actual.Properties.Configuration)
require.Equal(t, expected.Properties.Template, actual.Properties.Template)
}
30 changes: 30 additions & 0 deletions cli/azd/test/mocks/mockazsdk/container_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ func MockContainerAppGet(
return mockRequest
}

func MockContainerAppCreateOrUpdate(
mockContext *mocks.MockContext,
subscriptionId string,
resourceGroup string,
appName string,
containerApp *armappcontainers.ContainerApp,
) *http.Request {
mockRequest := &http.Request{}

mockContext.HttpClient.When(func(request *http.Request) bool {
return request.Method == http.MethodPut && strings.Contains(
request.URL.Path,
fmt.Sprintf(
"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.App/containerApps/%s",
subscriptionId,
resourceGroup,
appName,
),
)
}).RespondFn(func(request *http.Request) (*http.Response, error) {
*mockRequest = *request

response := armappcontainers.ContainerAppsClientCreateOrUpdateResponse{}

return mocks.CreateHttpResponseWithBody(request, http.StatusCreated, response)
})

return mockRequest
}

func MockContainerAppUpdate(
mockContext *mocks.MockContext,
subscriptionId string,
Expand Down
12 changes: 12 additions & 0 deletions cli/azd/test/mocks/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,15 @@ func CreateEmptyHttpResponse(request *http.Request, statusCode int) (*http.Respo
Body: http.NoBody,
}, nil
}

// ReadHttpBody reads the body of an HTTP request or response and converts it into the specified object
func ReadHttpBody(body io.ReadCloser, v any) error {
defer body.Close()

jsonBytes, err := io.ReadAll(body)
if err != nil {
return err
}

return json.Unmarshal(jsonBytes, v)
}

0 comments on commit 3ee64ee

Please sign in to comment.