in cli/azd/pkg/project/service_target_dotnet_containerapp.go [109:450]
func (at *dotnetContainerAppTarget) Deploy(
ctx context.Context,
serviceConfig *ServiceConfig,
packageOutput *ServicePackageResult,
targetResource *environment.TargetResource,
progress *async.Progress[ServiceProgress],
) (*ServiceDeployResult, error) {
if err := at.validateTargetResource(targetResource); err != nil {
return nil, fmt.Errorf("validating target resource: %w", err)
}
progress.SetProgress(NewServiceProgress("Logging in to registry"))
// Login, tag & push container image to ACR
dockerCreds, err := at.containerHelper.Credentials(ctx, serviceConfig, targetResource)
if err != nil {
return nil, fmt.Errorf("logging in to registry: %w", err)
}
progress.SetProgress(NewServiceProgress("Pushing container image"))
var remoteImageName string
var portNumber int
// This service target is shared across four different aspire resource types: "dockerfile.v0" (a reference to
// an project backed by a dockerfile), "container.v0" (a reference to a project backed by an existing container
// image), "project.v0" (a reference to a project backed by a .NET project), and "container.v1" (a reference
// to a project which might have an existing container image, or can provide a dockerfile).
// Depending on the type, we have different steps for pushing the container image.
//
// For the dockerfile.v0 and container.v1+dockerfile type, [DotNetImporter] arranges things such that we can
// leverage the existing support in `azd` for services backed by a Dockerfile.
// This causes the image to be built and pushed to ACR.
//
// For the container.v0 or container.v1+image type, we assume the container image specified by the manifest is
// public and just use it directly.
//
// For the project.v0 type, we use the .NET CLI to publish the container image to ACR.
//
// The name of the image that should be referenced in the manifest is stored in `remoteImageName` and presented
// to the deployment template as a parameter named `Image`.
if serviceConfig.Language == ServiceLanguageDocker {
res, err := at.containerHelper.Deploy(ctx, serviceConfig, packageOutput, targetResource, false, progress)
if err != nil {
return nil, err
}
remoteImageName = res.Details.(*dockerDeployResult).RemoteImageTag
} else if serviceConfig.DotNetContainerApp.ContainerImage != "" {
remoteImageName = serviceConfig.DotNetContainerApp.ContainerImage
} else {
imageName := fmt.Sprintf("%s:%s",
at.containerHelper.DefaultImageName(serviceConfig),
at.containerHelper.DefaultImageTag())
portNumber, err = at.dotNetCli.PublishContainer(
ctx,
serviceConfig.Path(),
"Release",
imageName,
dockerCreds.LoginServer,
dockerCreds.Username,
dockerCreds.Password)
if err != nil {
return nil, fmt.Errorf("publishing container: %w", err)
}
remoteImageName = fmt.Sprintf("%s/%s", dockerCreds.LoginServer, imageName)
}
progress.SetProgress(NewServiceProgress("Updating application"))
var manifestTemplate string
var armTemplate *azure.RawArmTemplate
var armParams azure.ArmParameters
appHostRoot := serviceConfig.DotNetContainerApp.AppHostPath
if f, err := os.Stat(appHostRoot); err == nil && !f.IsDir() {
appHostRoot = filepath.Dir(appHostRoot)
}
deploymentConfig := serviceConfig.DotNetContainerApp.Manifest.Resources[serviceConfig.Name].Deployment
useBicepForContainerApps := deploymentConfig != nil
projectName := serviceConfig.DotNetContainerApp.ProjectName
if useBicepForContainerApps {
bicepParamPath := filepath.Join(
appHostRoot, "infra", projectName, fmt.Sprintf("%s.tmpl.bicepparam", projectName))
if _, err := os.Stat(bicepParamPath); err == nil {
// read the file into manifestContents
contents, err := os.ReadFile(bicepParamPath)
if err != nil {
return nil, fmt.Errorf("reading container app manifest: %w", err)
}
manifestTemplate = string(contents)
} else {
// missing bicepparam template file, generate it
contents, _, err := apphost.ContainerAppManifestTemplateForProject(
serviceConfig.DotNetContainerApp.Manifest,
projectName,
apphost.AppHostOptions{},
)
if err != nil {
return nil, fmt.Errorf("generating container app manifest: %w", err)
}
manifestTemplate = contents
}
} else {
manifestPath := filepath.Join(
appHostRoot, "infra", fmt.Sprintf("%s.tmpl.yaml", projectName))
if _, err := os.Stat(manifestPath); err == nil {
log.Printf("using container app manifest from %s", manifestPath)
contents, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("reading container app manifest: %w", err)
}
manifestTemplate = string(contents)
} else {
log.Printf(
"generating container app manifest from %s for project %s",
serviceConfig.DotNetContainerApp.AppHostPath,
projectName)
generatedManifest, _, err := apphost.ContainerAppManifestTemplateForProject(
serviceConfig.DotNetContainerApp.Manifest,
projectName,
apphost.AppHostOptions{},
)
if err != nil {
return nil, fmt.Errorf("generating container app manifest: %w", err)
}
manifestTemplate = generatedManifest
}
}
log.Printf("Resolve the manifest template for project %s", projectName)
fns := &containerAppTemplateManifestFuncs{
ctx: ctx,
manifest: serviceConfig.DotNetContainerApp.Manifest,
targetResource: targetResource,
containerAppService: at.containerAppService,
cosmosDbService: at.cosmosDbService,
sqlDbService: at.sqlDbService,
env: at.env,
keyvaultService: at.keyvaultService,
}
funcMap := template.FuncMap{
"urlHost": fns.UrlHost,
"parameter": fns.Parameter,
"parameterWithDefault": fns.ParameterWithDefault,
// securedParameter gets a parameter the same way as parameter, but supporting the securedParameter
// allows to update the logic of pulling secret parameters in the future, if azd changes the way it
// stores the parameter value.
"securedParameter": fns.Parameter,
"secretOutput": fns.kvSecret,
"targetPortOrDefault": func(targetPortFromManifest int) int {
// portNumber is 0 for dockerfile.v0, so we use the targetPort from the manifest
if portNumber == 0 {
return targetPortFromManifest
}
return portNumber
},
}
var inputs map[string]any
// inputs are auto-gen during provision and saved to env-config
if has, err := at.env.Config.GetSection("inputs", &inputs); err != nil {
return nil, fmt.Errorf("failed to get inputs section: %w", err)
} else if !has {
inputs = make(map[string]any)
}
tmpl, err := template.New("manifest template").
Option("missingkey=error").
Funcs(funcMap).
Parse(manifestTemplate)
if err != nil {
return nil, fmt.Errorf("failing parsing manifest template: %w", err)
}
builder := strings.Builder{}
err = tmpl.Execute(&builder, struct {
Env map[string]string
Image string
Inputs map[string]any
}{
Env: at.env.Dotenv(),
Image: remoteImageName,
Inputs: inputs,
})
if err != nil {
return nil, fmt.Errorf("failed executing template file: %w", err)
}
aspireDeploymentType := azapi.AzureResourceTypeContainerApp
resourceName := serviceConfig.Name
if useBicepForContainerApps {
// Compile the bicep template
compiled, params, err := func() (azure.RawArmTemplate, azure.ArmParameters, error) {
tempFolder, err := os.MkdirTemp("", fmt.Sprintf("%s-build*", projectName))
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("creating temporary build folder: %w", err)
}
defer func() {
_ = os.RemoveAll(tempFolder)
}()
// write bicepparam content to a new file in the temp folder
f, err := os.Create(filepath.Join(tempFolder, "main.bicepparam"))
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("creating bicepparam file: %w", err)
}
_, err = io.Copy(f, strings.NewReader(builder.String()))
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("writing bicepparam file: %w", err)
}
err = f.Close()
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("closing bicepparam file: %w", err)
}
// copy module to same path as bicepparam so it can be compiled from the temp folder
bicepSourceFileName := filepath.Base(*deploymentConfig.Path)
bicepContent, err := os.ReadFile(filepath.Join(appHostRoot, "infra", projectName, bicepSourceFileName))
if err != nil {
// when source bicep is not found, we generate it from the manifest
generatedBicep, err := apphost.ContainerSourceBicepContent(
serviceConfig.DotNetContainerApp.Manifest,
projectName,
apphost.AppHostOptions{},
)
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("generating bicep file: %w", err)
}
bicepContent = []byte(generatedBicep)
}
sourceFile, err := os.Create(filepath.Join(tempFolder, bicepSourceFileName))
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("creating bicep file: %w", err)
}
_, err = io.Copy(sourceFile, strings.NewReader(string(bicepContent)))
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("writing bicep file: %w", err)
}
err = sourceFile.Close()
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("closing bicep file: %w", err)
}
res, err := at.bicepCli.BuildBicepParam(ctx, f.Name(), at.env.Environ())
if err != nil {
return azure.RawArmTemplate{}, nil, fmt.Errorf("building container app bicep: %w", err)
}
type compiledBicepParamResult struct {
TemplateJson string `json:"templateJson"`
ParametersJson string `json:"parametersJson"`
}
var bicepParamOutput compiledBicepParamResult
if err := json.Unmarshal([]byte(res.Compiled), &bicepParamOutput); err != nil {
log.Printf(
"failed unmarshalling compiled bicepparam (err: %v), template contents:\n%s", err, res.Compiled)
return nil, nil, fmt.Errorf("failed unmarshalling arm template from json: %w", err)
}
var params azure.ArmParameterFile
if err := json.Unmarshal([]byte(bicepParamOutput.ParametersJson), ¶ms); err != nil {
log.Printf(
"failed unmarshalling compiled bicepparam parameters(err: %v), template contents:\n%s",
err,
res.Compiled)
return nil, nil, fmt.Errorf("failed unmarshalling arm parameters template from json: %w", err)
}
return azure.RawArmTemplate(bicepParamOutput.TemplateJson), params.Parameters, nil
}()
if err != nil {
return nil, err
}
armTemplate = &compiled
armParams = params
deploymentResult, err := at.deploymentService.DeployToResourceGroup(
ctx,
at.env.GetSubscriptionId(),
targetResource.ResourceGroupName(),
at.deploymentService.GenerateDeploymentName(serviceConfig.Name),
*armTemplate,
armParams,
nil, nil)
if err != nil {
return nil, fmt.Errorf("deploying bicep template: %w", err)
}
deploymentHostDetails, err := deploymentHost(deploymentResult)
if err != nil {
return nil, fmt.Errorf("getting deployment host type: %w", err)
}
resourceName = deploymentHostDetails.name
aspireDeploymentType = deploymentHostDetails.hostType
} else {
containerAppOptions := containerapps.ContainerAppOptions{
ApiVersion: serviceConfig.ApiVersion,
}
err = at.containerAppService.DeployYaml(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
serviceConfig.Name,
[]byte(builder.String()),
&containerAppOptions,
)
if err != nil {
return nil, fmt.Errorf("updating container app service: %w", err)
}
}
progress.SetProgress(NewServiceProgress("Fetching endpoints for service"))
target := environment.NewTargetResource(
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
resourceName,
string(aspireDeploymentType))
endpoints, err := at.Endpoints(ctx, serviceConfig, target)
if err != nil {
return nil, err
}
return &ServiceDeployResult{
Package: packageOutput,
TargetResourceId: azure.ContainerAppRID(
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
serviceConfig.Name,
),
Kind: ContainerAppTarget,
Endpoints: endpoints,
}, nil
}