Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DataProtection Runtime feature in ACA #3731

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cli/azd/pkg/apphost/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func Dockerfiles(manifest *Manifest) map[string]genDockerfile {
// ContainerAppManifestTemplateForProject returns the container app manifest template for a given project.
// It can be used (after evaluation) to deploy the service to a container app environment.
func ContainerAppManifestTemplateForProject(
manifest *Manifest, projectName string) (string, error) {
manifest *Manifest, projectName string, autoConfigureDataProtection bool) (string, error) {
generator := newInfraGenerator()

if err := generator.LoadManifest(manifest); err != nil {
Expand All @@ -123,7 +123,10 @@ func ContainerAppManifestTemplateForProject(

var buf bytes.Buffer

err := genTemplates.ExecuteTemplate(&buf, "containerApp.tmpl.yaml", generator.containerAppTemplateContexts[projectName])
tmplCtx := generator.containerAppTemplateContexts[projectName]
tmplCtx.AutoConfigureDataProtection = autoConfigureDataProtection

err := genTemplates.ExecuteTemplate(&buf, "containerApp.tmpl.yaml", tmplCtx)
if err != nil {
return "", fmt.Errorf("executing template: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/pkg/apphost/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestAspireEscaping(t *testing.T) {

for _, name := range []string{"api"} {
t.Run(name, func(t *testing.T) {
tmpl, err := ContainerAppManifestTemplateForProject(m, name)
tmpl, err := ContainerAppManifestTemplateForProject(m, name, false)
require.NoError(t, err)
snapshot.SnapshotT(t, tmpl)
})
Expand Down Expand Up @@ -162,7 +162,7 @@ func TestAspireBicepGeneration(t *testing.T) {

for _, name := range []string{"frontend"} {
t.Run(name, func(t *testing.T) {
tmpl, err := ContainerAppManifestTemplateForProject(m, name)
tmpl, err := ContainerAppManifestTemplateForProject(m, name, false)
require.NoError(t, err)
snapshot.SnapshotT(t, tmpl)
})
Expand All @@ -184,7 +184,7 @@ func TestAspireDockerGeneration(t *testing.T) {

for _, name := range []string{"nodeapp"} {
t.Run(name, func(t *testing.T) {
tmpl, err := ContainerAppManifestTemplateForProject(m, name)
tmpl, err := ContainerAppManifestTemplateForProject(m, name, false)
require.NoError(t, err)
snapshot.SnapshotT(t, tmpl)
})
Expand Down
13 changes: 7 additions & 6 deletions cli/azd/pkg/apphost/generate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,13 @@ type genBicepTemplateContext struct {
}

type genContainerAppManifestTemplateContext struct {
Name string
Ingress *genContainerAppIngress
Env map[string]string
Secrets map[string]string
KeyVaultSecrets map[string]string
Dapr *genContainerAppManifestTemplateContextDapr
Name string
Ingress *genContainerAppIngress
Env map[string]string
Secrets map[string]string
KeyVaultSecrets map[string]string
Dapr *genContainerAppManifestTemplateContextDapr
AutoConfigureDataProtection bool
}

type genProjectFileContext struct {
Expand Down
137 changes: 120 additions & 17 deletions cli/azd/pkg/containerapps/container_app.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package containerapps

import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"slices"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3"
azdinternal "github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
Expand Down Expand Up @@ -101,39 +108,92 @@ func (cas *containerAppService) GetIngressConfiguration(
}, nil
}

// apiVersionKey is the key that can be set in the root of a deployment yaml to control the API version used when creating
// or updating the container app. When unset, we use the default API version of the armappcontainers.ContainerAppsClient.
const apiVersionKey = "api-version"

func (cas *containerAppService) DeployYaml(
ctx context.Context,
subscriptionId string,
resourceGroupName string,
appName string,
containerAppYaml []byte,
) error {
appClient, err := cas.createContainerAppsClient(ctx, subscriptionId)
if err != nil {
return err
}

var obj map[string]any
if err := yaml.Unmarshal(containerAppYaml, &obj); err != nil {
return fmt.Errorf("decoding yaml: %w", err)
}

containerAppJson, err := json.Marshal(obj)
if err != nil {
panic("should not have failed")
}
var poller *runtime.Poller[armappcontainers.ContainerAppsClientCreateOrUpdateResponse]

var containerApp armappcontainers.ContainerApp
if err := json.Unmarshal(containerAppJson, &containerApp); err != nil {
return fmt.Errorf("converting to container app type: %w", err)
}
// The way we make the initial request depends on whether the apiVersion is specified in the YAML.
if apiVersion, ok := obj[apiVersionKey].(string); ok {
// When the apiVersion is specified, we need to use a custom policy to inject the apiVersion and body into the
// request. This is because the ContainerAppsClient is built for a specific api version and does not allow us to
// change it. The custom policy allows us to use the parts of the SDK around building the request URL and using
// the standard pipeline - but we have to use a policy to change the api-version header and inject the body since
// the armappcontainers.ContainerApp{} is also built for a specific api version.
customPolicy := &containerAppCustomApiVersionAndBodyPolicy{
apiVersion: apiVersion,
}

poller, err := appClient.BeginCreateOrUpdate(ctx, resourceGroupName, appName, containerApp, nil)
if err != nil {
return fmt.Errorf("applying manifest: %w", err)
appClient, err := cas.createContainerAppsClientWithPerCallPolicy(ctx, subscriptionId, customPolicy)
if err != nil {
return err
}

// Remove the apiVersion field from the object so it doesn't get injected into the request body. On the wire this
// is in a query parameter, not the body.
delete(obj, apiVersionKey)

containerAppJson, err := json.Marshal(obj)
if err != nil {
panic("should not have failed")
}

// Set the body injected by the policy to be the full container app JSON from the YAML.
customPolicy.body = (*json.RawMessage)(&containerAppJson)

// It doesn't matter what we configure here - the value is going to be overwritten by the custom policy. But we need
// to pass in a value, so use the zero value.
emptyApp := armappcontainers.ContainerApp{}

p, err := appClient.BeginCreateOrUpdate(ctx, resourceGroupName, appName, emptyApp, nil)
if err != nil {
return fmt.Errorf("applying manifest: %w", err)
}
poller = p

// Now that we've sent the request, clear the body so it is not injected on any subsequent requests (e.g. ones made
// by the poller when we poll).
customPolicy.body = nil
} else {
// When the apiVersion field is unset in the YAML, we can use the standard SDK to build the request and send it
// like normal.
appClient, err := cas.createContainerAppsClient(ctx, subscriptionId)
if err != nil {
return err
}

containerAppJson, err := json.Marshal(obj)
if err != nil {
panic("should not have failed")
}

var containerApp armappcontainers.ContainerApp
if err := json.Unmarshal(containerAppJson, &containerApp); err != nil {
return fmt.Errorf("converting to container app type: %w", err)
}

p, err := appClient.BeginCreateOrUpdate(ctx, resourceGroupName, appName, containerApp, nil)
if err != nil {
return fmt.Errorf("applying manifest: %w", err)
}

poller = p
}

_, err = poller.PollUntilDone(ctx, nil)
_, err := poller.PollUntilDone(ctx, nil)
if err != nil {
return fmt.Errorf("polling for container app update completion: %w", err)
}
Expand Down Expand Up @@ -337,6 +397,28 @@ func (cas *containerAppService) createContainerAppsClient(
return client, nil
}

func (cas *containerAppService) createContainerAppsClientWithPerCallPolicy(
ctx context.Context,
subscriptionId string,
policy policy.Policy,
) (*armappcontainers.ContainerAppsClient, error) {
credential, err := cas.credentialProvider.CredentialForSubscription(ctx, subscriptionId)
if err != nil {
return nil, err
}

// Clone the options so we don't modify the original - we don't want to inject this custom policy into every request.
options := *cas.armClientOptions
options.PerCallPolicies = append(slices.Clone(options.PerCallPolicies), policy)

client, err := armappcontainers.NewContainerAppsClient(subscriptionId, credential, &options)
if err != nil {
return nil, fmt.Errorf("creating ContainerApps client: %w", err)
}

return client, nil
}

func (cas *containerAppService) createRevisionsClient(
ctx context.Context,
subscriptionId string,
Expand All @@ -353,3 +435,24 @@ func (cas *containerAppService) createRevisionsClient(

return client, nil
}

type containerAppCustomApiVersionAndBodyPolicy struct {
apiVersion string
body *json.RawMessage
}

func (p *containerAppCustomApiVersionAndBodyPolicy) Do(req *policy.Request) (*http.Response, error) {
if p.body != nil {
reqQP := req.Raw().URL.Query()
reqQP.Set("api-version", p.apiVersion)
req.Raw().URL.RawQuery = reqQP.Encode()

log.Printf("setting body to %s", string(*p.body))

if err := req.SetBody(streaming.NopCloser(bytes.NewReader(*p.body)), "application/json"); err != nil {
return nil, fmt.Errorf("updating request body: %w", err)
}
}

return req.Next()
}
31 changes: 20 additions & 11 deletions cli/azd/pkg/project/dotnet_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"sync"

"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/ext"
Expand All @@ -27,10 +28,11 @@ type hostCheckResult struct {

// DotNetImporter is an importer that is able to import projects and infrastructure from a manifest produced by a .NET App.
type DotNetImporter struct {
dotnetCli dotnet.DotNetCli
console input.Console
lazyEnv *lazy.Lazy[*environment.Environment]
lazyEnvManager *lazy.Lazy[environment.Manager]
dotnetCli dotnet.DotNetCli
console input.Console
lazyEnv *lazy.Lazy[*environment.Environment]
lazyEnvManager *lazy.Lazy[environment.Manager]
alphaFeatureManager *alpha.FeatureManager

// TODO(ellismg): This cache exists because we end up needing the same manifest multiple times for a single logical
// operation and it is expensive to generate. We should consider if this is the correct location for the cache or if
Expand All @@ -55,14 +57,16 @@ func NewDotNetImporter(
console input.Console,
lazyEnv *lazy.Lazy[*environment.Environment],
lazyEnvManager *lazy.Lazy[environment.Manager],
alphaFeatureManager *alpha.FeatureManager,
) *DotNetImporter {
return &DotNetImporter{
dotnetCli: dotnetCli,
console: console,
lazyEnv: lazyEnv,
lazyEnvManager: lazyEnvManager,
cache: make(map[manifestCacheKey]*apphost.Manifest),
hostCheck: make(map[string]hostCheckResult),
dotnetCli: dotnetCli,
console: console,
lazyEnv: lazyEnv,
lazyEnvManager: lazyEnvManager,
alphaFeatureManager: alphaFeatureManager,
cache: make(map[manifestCacheKey]*apphost.Manifest),
hostCheck: make(map[string]hostCheckResult),
}
}

Expand Down Expand Up @@ -243,6 +247,8 @@ func (ai *DotNetImporter) Services(
return services, nil
}

var autoConfigureDataProtectionFeature = alpha.MustFeatureKey("aspire.autoConfigureDataProtection")

func (ai *DotNetImporter) SynthAllInfrastructure(
ctx context.Context, p *ProjectConfig, svcConfig *ServiceConfig,
) (fs.FS, error) {
Expand Down Expand Up @@ -291,11 +297,14 @@ func (ai *DotNetImporter) SynthAllInfrastructure(
return nil, err
}

autoConfigureDataProtection := ai.alphaFeatureManager.IsEnabled(autoConfigureDataProtectionFeature)

// writeManifestForResource writes the containerApp.tmpl.yaml for the given resource to the generated filesystem. The
// manifest is written to a file name "containerApp.tmpl.yaml" in the same directory as the project that produces the
// container we will deploy.
writeManifestForResource := func(name string, path string) error {
containerAppManifest, err := apphost.ContainerAppManifestTemplateForProject(manifest, name)
containerAppManifest, err := apphost.ContainerAppManifestTemplateForProject(
manifest, name, autoConfigureDataProtection)
if err != nil {
return fmt.Errorf("generating containerApp.tmpl.yaml for resource %s: %w", name, err)
}
Expand Down
7 changes: 7 additions & 0 deletions cli/azd/pkg/project/service_target_dotnet_containerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

"github.com/azure/azure-dev/cli/azd/internal/scaffold"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/azure"
Expand All @@ -37,6 +38,7 @@ type dotnetContainerAppTarget struct {
cosmosDbService cosmosdb.CosmosDbService
sqlDbService sqldb.SqlDbService
keyvaultService keyvault.KeyVaultService
alphaFeatureManager *alpha.FeatureManager
}

// NewDotNetContainerAppTarget creates the Service Target for a Container App that is written in .NET. Unlike
Expand All @@ -56,6 +58,7 @@ func NewDotNetContainerAppTarget(
cosmosDbService cosmosdb.CosmosDbService,
sqlDbService sqldb.SqlDbService,
keyvaultService keyvault.KeyVaultService,
alphaFeatureManager *alpha.FeatureManager,
) ServiceTarget {
return &dotnetContainerAppTarget{
env: env,
Expand All @@ -66,6 +69,7 @@ func NewDotNetContainerAppTarget(
cosmosDbService: cosmosDbService,
sqlDbService: sqlDbService,
keyvaultService: keyvaultService,
alphaFeatureManager: alphaFeatureManager,
}
}

Expand Down Expand Up @@ -159,6 +163,8 @@ func (at *dotnetContainerAppTarget) Deploy(
projectRoot = filepath.Dir(projectRoot)
}

autoConfigureDataProtection := at.alphaFeatureManager.IsEnabled(autoConfigureDataProtectionFeature)

manifestPath := filepath.Join(projectRoot, "manifests", "containerApp.tmpl.yaml")
if _, err := os.Stat(manifestPath); err == nil {
log.Printf("using container app manifest from %s", manifestPath)
Expand All @@ -178,6 +184,7 @@ func (at *dotnetContainerAppTarget) Deploy(
generatedManifest, err := apphost.ContainerAppManifestTemplateForProject(
serviceConfig.DotNetContainerApp.Manifest,
serviceConfig.DotNetContainerApp.ProjectName,
autoConfigureDataProtection,
)
if err != nil {
task.SetError(fmt.Errorf("generating container app manifest: %w", err))
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/resources/alpha_features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
description: "Support infrastructure deployments at resource group scope."
- id: infraSynth
description: "Enable the `infra synth` command to write generated infrastructure to disk."
- id: aspire.autoConfigureDataProtection
ellismg marked this conversation as resolved.
Show resolved Hide resolved
description: "Automatically configure data protection for Aspire deployments. May not be supported in all regions."
- id: aks.helm
description: "Enable Helm support for AKS deployments."
- id: aks.kustomize
Expand Down
Loading
Loading