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

Adds support to reference external or prebuilt image references #3269

Merged
merged 10 commits into from
Feb 8, 2024
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
Loading