Skip to content

Commit

Permalink
Adds support to reference external or prebuilt image references (#3269)
Browse files Browse the repository at this point in the history
Features
- Adds new image property at the root of service configuration only valid for containerapp
- Allows specifying a source image that will be used for the container app service
- Adds new image property within docker configuration options to set image name generation
- Works in tandem with tag property for generating final docker image path & tags

Notes
- When a service configuration does not contain a path to a source code project (no project defined) the docker configuration options allow developer to reference existing container images.
- When the image setting does not contain a registry endpoint then docker hub is assumed.
project and image are mutually exclusive and cannot be set at the same time
- Fully qualified image paths, ex) contoso.azurecr.io/myapp:latest can be referenced as well.
  • Loading branch information
wbreza authored Feb 8, 2024
1 parent 8fe3492 commit 5120941
Show file tree
Hide file tree
Showing 16 changed files with 1,233 additions and 177 deletions.
202 changes: 150 additions & 52 deletions cli/azd/pkg/project/container_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewContainerHelper(
}
}

// RegistryName returns the name of the container registry to use for the current environment from the following:
// RegistryName returns the name of the destination container registry to use for the current environment from the following:
// 1. AZURE_CONTAINER_REGISTRY_ENDPOINT environment variable
// 2. docker.registry from the service configuration
func (ch *ContainerHelper) RegistryName(ctx context.Context, serviceConfig *ServiceConfig) (string, error) {
Expand All @@ -60,7 +60,11 @@ func (ch *ContainerHelper) RegistryName(ctx context.Context, serviceConfig *Serv
registryName = yamlRegistryName
}

if registryName == "" {
// If the service provides its own code artifacts then the expectation is that an images needs to be built and
// pushed to a container registry.
// If the service does not provide its own code artifacts then the expectation is a registry is optional and
// an image can be referenced independently.
if serviceConfig.RelativePath != "" && registryName == "" {
return "", fmt.Errorf(
//nolint:lll
"could not determine container registry endpoint, ensure 'registry' has been set in the docker options or '%s' environment variable has been set",
Expand All @@ -71,39 +75,91 @@ func (ch *ContainerHelper) RegistryName(ctx context.Context, serviceConfig *Serv
return registryName, nil
}

// GeneratedImage returns the configured image from the service configuration
// or a default image name generated from the service name and environment name.
func (ch *ContainerHelper) GeneratedImage(
ctx context.Context,
serviceConfig *ServiceConfig,
) (*docker.ContainerImage, error) {
// Parse the image from azure.yaml configuration when available
configuredImage, err := serviceConfig.Docker.Image.Envsubst(ch.env.Getenv)
if err != nil {
return nil, fmt.Errorf("failed parsing 'image' from docker configuration, %w", err)
}

// Set default image name if not configured
if configuredImage == "" {
configuredImage = fmt.Sprintf("%s/%s-%s",
strings.ToLower(serviceConfig.Project.Name),
strings.ToLower(serviceConfig.Name),
strings.ToLower(ch.env.Name()),
)
}

parsedImage, err := docker.ParseContainerImage(configuredImage)
if err != nil {
return nil, fmt.Errorf("failed parsing configured image, %w", err)
}

if parsedImage.Tag == "" {
configuredTag, err := serviceConfig.Docker.Tag.Envsubst(ch.env.Getenv)
if err != nil {
return nil, fmt.Errorf("failed parsing 'tag' from docker configuration, %w", err)
}

// Set default tag if not configured
if configuredTag == "" {
configuredTag = fmt.Sprintf("azd-deploy-%d",
ch.clock.Now().Unix(),
)
}

parsedImage.Tag = configuredTag
}

// Set default registry if not configured
if parsedImage.Registry == "" {
// This can fail if called before provisioning the registry
configuredRegistry, err := ch.RegistryName(ctx, serviceConfig)
if err == nil {
parsedImage.Registry = configuredRegistry
}
}

return parsedImage, nil
}

// RemoteImageTag returns the remote image tag for the service configuration.
func (ch *ContainerHelper) RemoteImageTag(
ctx context.Context,
serviceConfig *ServiceConfig,
localImageTag string,
) (string, error) {
loginServer, err := ch.RegistryName(ctx, serviceConfig)
registryName, err := ch.RegistryName(ctx, serviceConfig)
if err != nil {
return "", err
}

return fmt.Sprintf(
"%s/%s",
loginServer,
localImageTag,
), nil
containerImage, err := docker.ParseContainerImage(localImageTag)
if err != nil {
return "", err
}

if registryName != "" {
containerImage.Registry = registryName
}

return containerImage.Remote(), nil
}

// LocalImageTag returns the local image tag for the service configuration.
func (ch *ContainerHelper) LocalImageTag(ctx context.Context, serviceConfig *ServiceConfig) (string, error) {
configuredTag, err := serviceConfig.Docker.Tag.Envsubst(ch.env.Getenv)
configuredImage, err := ch.GeneratedImage(ctx, serviceConfig)
if err != nil {
return "", err
}

if configuredTag != "" {
return configuredTag, nil
}

return fmt.Sprintf("%s/%s-%s:azd-deploy-%d",
strings.ToLower(serviceConfig.Project.Name),
strings.ToLower(serviceConfig.Name),
strings.ToLower(ch.env.Name()),
ch.clock.Now().Unix(),
), nil
return configuredImage.Local(), nil
}

func (ch *ContainerHelper) RequiredExternalTools(context.Context) []tools.ExternalTool {
Expand All @@ -122,7 +178,14 @@ func (ch *ContainerHelper) Login(
return "", err
}

return loginServer, ch.containerRegistryService.Login(ctx, targetResource.SubscriptionId(), loginServer)
// Only perform automatic login for ACR
// Other registries require manual login via external 'docker login' command
hostParts := strings.Split(loginServer, ".")
if len(hostParts) == 1 || strings.Contains(loginServer, "azurecr.io") {
return loginServer, ch.containerRegistryService.Login(ctx, targetResource.SubscriptionId(), loginServer)
}

return loginServer, nil
}

func (ch *ContainerHelper) Credentials(
Expand Down Expand Up @@ -150,57 +213,92 @@ func (ch *ContainerHelper) Deploy(
return async.RunTaskWithProgress(
func(task *async.TaskContextWithProgress[*ServiceDeployResult, ServiceProgress]) {
// Get ACR Login Server
loginServer, err := ch.RegistryName(ctx, serviceConfig)
registryName, err := ch.RegistryName(ctx, serviceConfig)
if err != nil {
task.SetError(err)
return
}

localImageTag := packageOutput.PackagePath
packageDetails, ok := packageOutput.Details.(*dockerPackageResult)
if ok && packageDetails != nil {
localImageTag = packageDetails.ImageTag
}
var sourceImage string
targetImage := packageOutput.PackagePath

if localImageTag == "" {
task.SetError(errors.New("failed retrieving package result details"))
return
packageDetails := packageOutput.Details.(*dockerPackageResult)
if packageDetails != nil {
sourceImage = packageDetails.SourceImage
targetImage = packageDetails.TargetImage
}

// Tag image
// Get remote tag from the container helper then call docker cli tag command
remoteTag, err := ch.RemoteImageTag(ctx, serviceConfig, localImageTag)
if err != nil {
task.SetError(fmt.Errorf("getting remote image tag: %w", err))
// If we don't have a registry specified and the service does not reference a project path
// then we are referencing a public/pre-existing image and don't have anything to tag or push
if registryName == "" && serviceConfig.RelativePath == "" && sourceImage != "" {
task.SetResult(&ServiceDeployResult{
Package: packageOutput,
Details: &dockerDeployResult{
RemoteImageTag: sourceImage,
},
})
return
}

task.SetProgress(NewServiceProgress("Tagging container image"))
if err := ch.docker.Tag(ctx, serviceConfig.Path(), localImageTag, remoteTag); err != nil {
task.SetError(err)
if targetImage == "" {
task.SetError(errors.New("failed retrieving package result details"))
return
}

log.Printf("logging into container registry '%s'\n", loginServer)
task.SetProgress(NewServiceProgress("Logging into container registry"))
err = ch.containerRegistryService.Login(ctx, targetResource.SubscriptionId(), loginServer)
if err != nil {
task.SetError(err)
return
}
// Default to the local image tag
remoteImage := targetImage

// Push image.
log.Printf("pushing %s to registry", remoteTag)
task.SetProgress(NewServiceProgress("Pushing container image"))
if err := ch.docker.Push(ctx, serviceConfig.Path(), remoteTag); err != nil {
task.SetError(err)
return
// If a registry has not been defined then there is no need to tag or push any images
if registryName != "" {
// When the project does not contain source and we are using an external image we first need to pull the image
// before we're able to push it to a remote registry
// In most cases this pull will have already been part of the package step
if packageDetails != nil && serviceConfig.RelativePath == "" {
task.SetProgress(NewServiceProgress("Pulling container image"))
err = ch.docker.Pull(ctx, sourceImage)
if err != nil {
task.SetError(fmt.Errorf("pulling image: %w", err))
return
}
}

// Tag image
// Get remote remoteImageWithTag from the container helper then call docker cli remoteImageWithTag command
remoteImageWithTag, err := ch.RemoteImageTag(ctx, serviceConfig, targetImage)
if err != nil {
task.SetError(fmt.Errorf("getting remote image tag: %w", err))
return
}

remoteImage = remoteImageWithTag

task.SetProgress(NewServiceProgress("Tagging container image"))
if err := ch.docker.Tag(ctx, serviceConfig.Path(), targetImage, remoteImage); err != nil {
task.SetError(err)
return
}

log.Printf("logging into container registry '%s'\n", registryName)
task.SetProgress(NewServiceProgress("Logging into container registry"))
err = ch.containerRegistryService.Login(ctx, targetResource.SubscriptionId(), registryName)
if err != nil {
task.SetError(err)
return
}

// Push image.
log.Printf("pushing %s to registry", remoteImage)
task.SetProgress(NewServiceProgress("Pushing container image"))
if err := ch.docker.Push(ctx, serviceConfig.Path(), remoteImage); err != nil {
task.SetError(err)
return
}
}

if writeImageToEnv {
// Save the name of the image we pushed into the environment with a well known key.
log.Printf("writing image name to environment")
ch.env.SetServiceProperty(serviceConfig.Name, "IMAGE_NAME", remoteTag)
ch.env.SetServiceProperty(serviceConfig.Name, "IMAGE_NAME", remoteImage)

if err := ch.envManager.Save(ctx, ch.env); err != nil {
task.SetError(fmt.Errorf("saving image name to environment: %w", err))
Expand All @@ -211,7 +309,7 @@ func (ch *ContainerHelper) Deploy(
task.SetResult(&ServiceDeployResult{
Package: packageOutput,
Details: &dockerDeployResult{
RemoteImageTag: remoteTag,
RemoteImageTag: remoteImage,
},
})
})
Expand Down
Loading

0 comments on commit 5120941

Please sign in to comment.