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
183 changes: 137 additions & 46 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,82 @@ func (ch *ContainerHelper) RegistryName(ctx context.Context, serviceConfig *Serv
return registryName, nil
}

// ConfiguredImage returns the configured image from the service configuration
// or a default image name generated from the service name and environment name.
func (ch *ContainerHelper) ConfiguredImage(
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)
}

parsedImage, err := docker.ParseContainerImage(configuredImage)
if err == nil {
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)
}

parsedImage.Tag = configuredTag
}

return parsedImage, nil
}

configuredRegistry, err := ch.RegistryName(ctx, serviceConfig)
if err != nil {
return nil, err
}

// Otherwise return a image name generated from default strategy
defaultImage := &docker.ContainerImage{
Registry: configuredRegistry,
Repository: fmt.Sprintf("%s/%s-%s",
strings.ToLower(serviceConfig.Project.Name),
strings.ToLower(serviceConfig.Name),
strings.ToLower(ch.env.Name()),
),
Tag: fmt.Sprintf("azd-deploy-%d",
ch.clock.Now().Unix(),
),
}

return defaultImage, 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
}

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.ConfiguredImage(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 +169,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,7 +204,7 @@ 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
Expand All @@ -162,39 +216,76 @@ func (ch *ContainerHelper) Deploy(
localImageTag = packageDetails.ImageTag
}

if localImageTag == "" {
task.SetError(errors.New("failed retrieving package result details"))
// 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 != "" {
task.SetResult(&ServiceDeployResult{
Package: packageOutput,
Details: &dockerDeployResult{
RemoteImageTag: localImageTag,
},
})
return
}

// 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 localImageTag == "" {
task.SetError(errors.New("failed retrieving package result details"))
return
}

task.SetProgress(NewServiceProgress("Tagging container image"))
if err := ch.docker.Tag(ctx, serviceConfig.Path(), localImageTag, remoteTag); err != nil {
task.SetError(err)
return
}
// Default to the local image tag
remoteTag := localImageTag

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
}
// 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
if serviceConfig.RelativePath == "" {
sourceImage, err := ch.ConfiguredImage(ctx, serviceConfig)
if err != nil {
task.SetError(fmt.Errorf("getting configured image: %w", err))
return
}

// 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
task.SetProgress(NewServiceProgress("Pulling container image"))
err = ch.docker.Pull(ctx, sourceImage.Remote())
if err != nil {
task.SetError(fmt.Errorf("pulling image: %w", err))
return
}
}

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

remoteTag = tag

task.SetProgress(NewServiceProgress("Tagging container image"))
if err := ch.docker.Tag(ctx, serviceConfig.Path(), localImageTag, remoteTag); 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", remoteTag)
task.SetProgress(NewServiceProgress("Pushing container image"))
if err := ch.docker.Push(ctx, serviceConfig.Path(), remoteTag); err != nil {
task.SetError(err)
return
}
}

if writeImageToEnv {
Expand Down
Loading
Loading