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

dotnet: Support dotnet publish to produce container image #4573

Merged
merged 1 commit into from
Nov 22, 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
2 changes: 1 addition & 1 deletion cli/azd/internal/cmd/add/add_configure_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func addServiceAsResource(
if _, err := os.Stat(filepath.Join(svc.RelativePath, "Dockerfile")); errors.Is(err, os.ErrNotExist) {
// default builder always specifies port 80
props.Port = 80
if svc.Language == project.ServiceLanguageJava {
if svc.Language == project.ServiceLanguageJava || svc.Language.IsDotNet() {
props.Port = 8080
}
}
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/repository/app_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestInitializer_prjConfigFromDetect(t *testing.T) {
Type: project.ResourceTypeHostContainerApp,
Name: "dotnet",
Props: project.ContainerAppProps{
Port: 80,
Port: 8080,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/repository/infra_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func PromptPort(
name string,
svc appdetect.Project) (int, error) {
if svc.Docker == nil || svc.Docker.Path == "" { // using default builder from azd
if svc.Language == appdetect.Java {
if svc.Language == appdetect.Java || svc.Language == appdetect.DotNet {
return 8080, nil
}
return 80, nil
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/repository/infra_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestInitializer_infraSpecFromDetect(t *testing.T) {
Services: []scaffold.ServiceSpec{
{
Name: "dotnet",
Port: 80,
Port: 8080,
Backend: &scaffold.Backend{},
},
},
Expand Down
48 changes: 47 additions & 1 deletion cli/azd/pkg/project/container_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/azcli"
"github.com/azure/azure-dev/cli/azd/pkg/tools/docker"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/benbjohnson/clock"
"github.com/sethvargo/go-retry"
)
Expand All @@ -32,6 +33,7 @@ type ContainerHelper struct {
remoteBuildManager *containerregistry.RemoteBuildManager
containerRegistryService azcli.ContainerRegistryService
docker *docker.Cli
dotNetCli *dotnet.Cli
clock clock.Clock
console input.Console
cloud *cloud.Cloud
Expand All @@ -44,6 +46,7 @@ func NewContainerHelper(
containerRegistryService azcli.ContainerRegistryService,
remoteBuildManager *containerregistry.RemoteBuildManager,
docker *docker.Cli,
dotNetCli *dotnet.Cli,
console input.Console,
cloud *cloud.Cloud,
) *ContainerHelper {
Expand All @@ -53,6 +56,7 @@ func NewContainerHelper(
remoteBuildManager: remoteBuildManager,
containerRegistryService: containerRegistryService,
docker: docker,
dotNetCli: dotNetCli,
clock: clock,
console: console,
cloud: cloud,
Expand Down Expand Up @@ -194,6 +198,10 @@ func (ch *ContainerHelper) RequiredExternalTools(ctx context.Context, serviceCon
return []tools.ExternalTool{}
}

if useDotnetPublishForDockerBuild(serviceConfig) {
return []tools.ExternalTool{ch.dotNetCli}
}

return []tools.ExternalTool{ch.docker}
}

Expand Down Expand Up @@ -270,6 +278,8 @@ func (ch *ContainerHelper) Deploy(

if serviceConfig.Docker.RemoteBuild {
remoteImage, err = ch.runRemoteBuild(ctx, serviceConfig, targetResource, progress)
} else if useDotnetPublishForDockerBuild(serviceConfig) {
remoteImage, err = ch.runDotnetPublish(ctx, serviceConfig, targetResource, progress)
} else {
remoteImage, err = ch.runLocalBuild(ctx, serviceConfig, packageOutput, progress)
}
Expand Down Expand Up @@ -382,7 +392,8 @@ func (ch *ContainerHelper) runLocalBuild(
return remoteImage, nil
}

// runLocalBuild builds the image using a remote azure container registry and tags it. It returns the full remote image name.
// runRemoteBuild builds the image using a remote azure container registry and tags it.
// It returns the full remote image name.
func (ch *ContainerHelper) runRemoteBuild(
ctx context.Context,
serviceConfig *ServiceConfig,
Expand Down Expand Up @@ -473,6 +484,41 @@ func (ch *ContainerHelper) runRemoteBuild(
return imageName, nil
}

// runDotnetPublish builds and publishes the container image using `dotnet publish`. It returns the full remote image name.
func (ch *ContainerHelper) runDotnetPublish(
ctx context.Context,
serviceConfig *ServiceConfig,
target *environment.TargetResource,
progress *async.Progress[ServiceProgress],
) (string, error) {
progress.SetProgress(NewServiceProgress("Logging into registry"))

dockerCreds, err := ch.Credentials(ctx, serviceConfig, target)
if err != nil {
return "", fmt.Errorf("logging in to registry: %w", err)
}

progress.SetProgress(NewServiceProgress("Publishing container image"))

imageName := fmt.Sprintf("%s:%s",
ch.DefaultImageName(serviceConfig),
ch.DefaultImageTag())

_, err = ch.dotNetCli.PublishContainer(
ellismg marked this conversation as resolved.
Show resolved Hide resolved
ctx,
serviceConfig.Path(),
"Release",
imageName,
dockerCreds.LoginServer,
dockerCreds.Username,
dockerCreds.Password)
if err != nil {
return "", fmt.Errorf("publishing container: %w", err)
}

return fmt.Sprintf("%s/%s", dockerCreds.LoginServer, imageName), nil
}

type dockerDeployResult struct {
RemoteImageTag string
}
19 changes: 11 additions & 8 deletions cli/azd/pkg/project/container_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools/azcli"
"github.com/azure/azure-dev/cli/azd/pkg/tools/docker"
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockenv"
"github.com/benbjohnson/clock"
Expand Down Expand Up @@ -62,7 +63,7 @@ func Test_ContainerHelper_LocalImageTag(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := environment.NewWithValues("dev", map[string]string{})
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())
serviceConfig.Docker = tt.dockerConfig

tag, err := containerHelper.LocalImageTag(*mockContext.Context, serviceConfig)
Expand Down Expand Up @@ -111,7 +112,7 @@ func Test_ContainerHelper_RemoteImageTag(t *testing.T) {

mockContext := mocks.NewMockContext(context.Background())
env := environment.NewWithValues("dev", map[string]string{})
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -138,7 +139,7 @@ func Test_ContainerHelper_Resolve_RegistryName(t *testing.T) {
environment.ContainerRegistryEndpointEnvVarName: "contoso.azurecr.io",
})
envManager := &mockenv.MockEnvManager{}
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
registryName, err := containerHelper.RegistryName(*mockContext.Context, serviceConfig)

Expand All @@ -150,7 +151,7 @@ func Test_ContainerHelper_Resolve_RegistryName(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
env := environment.NewWithValues("dev", map[string]string{})
envManager := &mockenv.MockEnvManager{}
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
serviceConfig.Docker.Registry = osutil.NewExpandableString("contoso.azurecr.io")
registryName, err := containerHelper.RegistryName(*mockContext.Context, serviceConfig)
Expand All @@ -164,7 +165,7 @@ func Test_ContainerHelper_Resolve_RegistryName(t *testing.T) {
env := environment.NewWithValues("dev", map[string]string{})
env.DotenvSet("MY_CUSTOM_REGISTRY", "custom.azurecr.io")
envManager := &mockenv.MockEnvManager{}
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
serviceConfig.Docker.Registry = osutil.NewExpandableString("${MY_CUSTOM_REGISTRY}")
registryName, err := containerHelper.RegistryName(*mockContext.Context, serviceConfig)
Expand All @@ -177,7 +178,7 @@ func Test_ContainerHelper_Resolve_RegistryName(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
env := environment.NewWithValues("dev", map[string]string{})
envManager := &mockenv.MockEnvManager{}
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, envManager, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())
serviceConfig := createTestServiceConfig("./src/api", ContainerAppTarget, ServiceLanguageTypeScript)
registryName, err := containerHelper.RegistryName(*mockContext.Context, serviceConfig)

Expand Down Expand Up @@ -334,6 +335,7 @@ func Test_ContainerHelper_Deploy(t *testing.T) {
mockResults := setupDockerMocks(mockContext)
env := environment.NewWithValues("dev", map[string]string{})
dockerCli := docker.NewCli(mockContext.CommandRunner)
dotnetCli := dotnet.NewCli(mockContext.CommandRunner)
envManager := &mockenv.MockEnvManager{}
envManager.On("Save", *mockContext.Context, env).Return(nil)

Expand All @@ -347,6 +349,7 @@ func Test_ContainerHelper_Deploy(t *testing.T) {
mockContainerRegistryService,
nil,
dockerCli,
dotnetCli,
mockContext.Console,
cloud.AzurePublic(),
)
Expand Down Expand Up @@ -411,7 +414,7 @@ func Test_ContainerHelper_Deploy(t *testing.T) {
func Test_ContainerHelper_ConfiguredImage(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
env := environment.NewWithValues("dev", map[string]string{})
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, cloud.AzurePublic())
containerHelper := NewContainerHelper(env, nil, clock.NewMock(), nil, nil, nil, nil, nil, cloud.AzurePublic())

tests := []struct {
name string
Expand Down Expand Up @@ -597,7 +600,7 @@ func Test_ContainerHelper_Credential_Retry(t *testing.T) {
defaultCredentialsRetryDelay = 1 * time.Millisecond

containerHelper := NewContainerHelper(
env, envManager, clock.NewMock(), mockContainerService, nil, nil, nil, cloud.AzurePublic())
env, envManager, clock.NewMock(), mockContainerService, nil, nil, nil, nil, cloud.AzurePublic())

serviceConfig := createTestServiceConfig("path", ContainerAppTarget, ServiceLanguageDotNet)
serviceConfig.Docker.Registry = osutil.NewExpandableString("contoso.azurecr.io")
Expand Down
4 changes: 4 additions & 0 deletions cli/azd/pkg/project/framework_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,7 @@ func validatePackageOutput(packagePath string) error {

return nil
}

func (slk ServiceLanguageKind) IsDotNet() bool {
return slk == ServiceLanguageDotNet || slk == ServiceLanguageCsharp || slk == ServiceLanguageFsharp
}
51 changes: 39 additions & 12 deletions cli/azd/pkg/project/framework_service_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path/filepath"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/appdetect"
"github.com/azure/azure-dev/cli/azd/internal/tracing"
Expand Down Expand Up @@ -163,12 +164,8 @@ func (p *dockerProject) Requirements() FrameworkRequirements {
}

// Gets the required external tools for the project
func (p *dockerProject) RequiredExternalTools(_ context.Context, sc *ServiceConfig) []tools.ExternalTool {
if sc.Docker.RemoteBuild {
return []tools.ExternalTool{}
}

return []tools.ExternalTool{p.docker}
func (p *dockerProject) RequiredExternalTools(ctx context.Context, sc *ServiceConfig) []tools.ExternalTool {
return p.containerHelper.RequiredExternalTools(ctx, sc)
}

// Initializes the docker project
Expand Down Expand Up @@ -199,7 +196,7 @@ func (p *dockerProject) Build(
restoreOutput *ServiceRestoreResult,
progress *async.Progress[ServiceProgress],
) (*ServiceBuildResult, error) {
if serviceConfig.Docker.RemoteBuild {
if serviceConfig.Docker.RemoteBuild || useDotnetPublishForDockerBuild(serviceConfig) {
ellismg marked this conversation as resolved.
Show resolved Hide resolved
return &ServiceBuildResult{Restore: restoreOutput}, nil
}

Expand Down Expand Up @@ -275,12 +272,12 @@ func (p *dockerProject) Build(
strings.ToLower(serviceConfig.Name),
)

path := dockerOptions.Path
if !filepath.IsAbs(path) {
path = filepath.Join(serviceConfig.Path(), path)
dockerfilePath := dockerOptions.Path
if !filepath.IsAbs(dockerfilePath) {
dockerfilePath = filepath.Join(serviceConfig.Path(), dockerfilePath)
}

_, err = os.Stat(path)
_, err = os.Stat(dockerfilePath)
if errors.Is(err, os.ErrNotExist) && serviceConfig.Docker.Path == "" {
// Build the container from source when:
// 1. No Dockerfile path is specified, and
Expand Down Expand Up @@ -341,13 +338,43 @@ func (p *dockerProject) Build(
}, nil
}

func useDotnetPublishForDockerBuild(serviceConfig *ServiceConfig) bool {
if serviceConfig.useDotNetPublishForDockerBuild != nil {
ellismg marked this conversation as resolved.
Show resolved Hide resolved
return *serviceConfig.useDotNetPublishForDockerBuild
}

serviceConfig.useDotNetPublishForDockerBuild = to.Ptr(false)

if serviceConfig.Language.IsDotNet() {
projectPath := serviceConfig.Path()

dockerOptions := getDockerOptionsWithDefaults(serviceConfig.Docker)

dockerfilePath := dockerOptions.Path
if !filepath.IsAbs(dockerfilePath) {
s, err := os.Stat(projectPath)
if err == nil && s.IsDir() {
dockerfilePath = filepath.Join(projectPath, dockerfilePath)
} else {
dockerfilePath = filepath.Join(filepath.Dir(projectPath), dockerfilePath)
}
}

if _, err := os.Stat(dockerfilePath); errors.Is(err, os.ErrNotExist) {
serviceConfig.useDotNetPublishForDockerBuild = to.Ptr(true)
}
}

return *serviceConfig.useDotNetPublishForDockerBuild
}

func (p *dockerProject) Package(
ctx context.Context,
serviceConfig *ServiceConfig,
buildOutput *ServiceBuildResult,
progress *async.Progress[ServiceProgress],
) (*ServicePackageResult, error) {
if serviceConfig.Docker.RemoteBuild {
if serviceConfig.Docker.RemoteBuild || useDotnetPublishForDockerBuild(serviceConfig) {
return &ServicePackageResult{Build: buildOutput}, nil
}

Expand Down
Loading
Loading