Skip to content
Open
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
1 change: 1 addition & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ words:
- azdignore
- gofmt
- golangci
- golangtools
- lightspeed
- runewidth
- toplevel
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/azure/azure-dev/cli/azd/pkg/tools/git"
"github.com/azure/azure-dev/cli/azd/pkg/tools/github"
golangtools "github.com/azure/azure-dev/cli/azd/pkg/tools/golang"
"github.com/azure/azure-dev/cli/azd/pkg/tools/javac"
"github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl"
"github.com/azure/azure-dev/cli/azd/pkg/tools/language"
Expand Down Expand Up @@ -723,6 +724,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
container.MustRegisterSingleton(dotnet.NewCli)
container.MustRegisterSingleton(git.NewCli)
container.MustRegisterSingleton(github.NewGitHubCli)
container.MustRegisterSingleton(golangtools.NewCli)
container.MustRegisterSingleton(javac.NewCli)
container.MustRegisterSingleton(kubectl.NewCli)
container.MustRegisterSingleton(maven.NewCli)
Expand Down Expand Up @@ -809,6 +811,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
project.ServiceLanguageJavaScript: project.NewNodeProject,
project.ServiceLanguageTypeScript: project.NewNodeProject,
project.ServiceLanguageJava: project.NewMavenProject,
project.ServiceLanguageGo: project.NewGoProject,
project.ServiceLanguageDocker: project.NewDockerProject,
project.ServiceLanguageSwa: project.NewSwaProject,
project.ServiceLanguageCustom: project.NewCustomProject,
Expand Down
10 changes: 9 additions & 1 deletion cli/azd/pkg/project/framework_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,23 @@ const (
ServiceLanguageTypeScript ServiceLanguageKind = "ts"
ServiceLanguagePython ServiceLanguageKind = "python"
ServiceLanguageJava ServiceLanguageKind = "java"
ServiceLanguageGo ServiceLanguageKind = "go"
ServiceLanguageDocker ServiceLanguageKind = "docker"
ServiceLanguageSwa ServiceLanguageKind = "swa"
ServiceLanguageCustom ServiceLanguageKind = "custom"
)

func parseServiceLanguage(kind ServiceLanguageKind) (ServiceLanguageKind, error) {
// aliases
// Resolve common shorthand aliases that users may write in azure.yaml.
// The canonical constants (e.g. "javascript", "typescript") already match what
// users typically write, so only languages where a shorter/alternate name is
// commonly used need explicit aliases here.
if string(kind) == "py" {
return ServiceLanguagePython, nil
}
if string(kind) == "golang" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd to me that we are saying "here's the list of constants that are valid" above (ServiceLanguageKind), but then comparing against arbitrary strings here. I get that it's aliases, but it had to be explained to me - it's not clear why we end up with these variations, or why we only have to handle py and golang (for instance, why not 'js', and 'javascript' (ie, where do these aliases come from?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I've expanded the comment to explain that aliases exist for common shorthand users may write in azure.yaml (e.g. py for python, golang for go). The canonical constant names like javascript/typescript already match typical usage so they don't need separate aliases. Fixed in 1fe9bd5.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But where do you decide that something is a 'common' alias? Is it telemetry-based, or intuition?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we accept golang anywhere else?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, go/golang is new as language for all azd.

We don't have a way to decide about alias. I'm fine removing the alias if you only want go. The alias is coming automatically after looking what we do for python/py and dotnet/net --

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "golang" alias is accepted here, but the azure.yaml schemas (schemas/v1.0/azure.yaml.json and schemas/alpha/azure.yaml.json) only list "go". A user who writes language: golang will get a schema validation warning in their editor even though azd accepts it.

The existing py/python precedent keeps both forms in the schema enum. Suggest either adding "golang" to both schema enums to match, or dropping the alias — to keep schema and behavior in sync.

return ServiceLanguageGo, nil
}

switch kind {
case ServiceLanguageNone,
Expand All @@ -43,6 +50,7 @@ func parseServiceLanguage(kind ServiceLanguageKind) (ServiceLanguageKind, error)
ServiceLanguageTypeScript,
ServiceLanguagePython,
ServiceLanguageJava,
ServiceLanguageGo,
ServiceLanguageDocker,
ServiceLanguageCustom:
// Excluding ServiceLanguageSwa since it is implicitly derived currently,
Expand Down
234 changes: 234 additions & 0 deletions cli/azd/pkg/project/framework_service_go.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Comment thread
vhvb1989 marked this conversation as resolved.
// Licensed under the MIT License.

package project

import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/golang"
"github.com/otiai10/copy"
)

const (
// goBinaryName is the compiled binary name for Azure Functions Go worker on Linux.
goBinaryName = "app"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goBinaryName = "app" (and the hardcoded GOOS=linux/GOARCH=amd64 in Build) encode the Flex Consumption Go worker contract. That's fine for the current target, but the dependency on the platform-expected binary name is implicit. Consider a short comment linking to the platform/runtime doc that defines this contract, so a future change (e.g. a different worker name, or arm64 support) has a clear source of truth. Non-blocking.

)

type goProject struct {
env *environment.Environment
goCli *golang.Cli
}

// NewGoProject creates a new instance of a Go project framework service.
func NewGoProject(
env *environment.Environment,
goCli *golang.Cli,
) FrameworkService {
return &goProject{
env: env,
goCli: goCli,
}
}

func (gp *goProject) Requirements() FrameworkRequirements {
return FrameworkRequirements{
Package: FrameworkPackageRequirements{
RequireRestore: true,
RequireBuild: true,
},
Comment thread
vhvb1989 marked this conversation as resolved.
}
}

// RequiredExternalTools returns the Go CLI as a required tool.
func (gp *goProject) RequiredExternalTools(
_ context.Context,
_ *ServiceConfig,
) []tools.ExternalTool {
return []tools.ExternalTool{gp.goCli}
}

// Initialize is a no-op for Go projects.
func (gp *goProject) Initialize(
ctx context.Context,
serviceConfig *ServiceConfig,
) error {
return nil
Comment thread
vhvb1989 marked this conversation as resolved.
}

// Restore downloads Go module dependencies.
func (gp *goProject) Restore(
ctx context.Context,
serviceConfig *ServiceConfig,
serviceContext *ServiceContext,
progress *async.Progress[ServiceProgress],
) (*ServiceRestoreResult, error) {
progress.SetProgress(NewServiceProgress("Downloading Go modules"))
if err := gp.goCli.ModDownload(ctx, serviceConfig.Path(), gp.env.Environ()); err != nil {
return nil, fmt.Errorf("restoring Go dependencies: %w", err)
}

return &ServiceRestoreResult{
Artifacts: ArtifactCollection{
{
Kind: ArtifactKindDirectory,
Location: serviceConfig.Path(),
LocationKind: LocationKindLocal,
Metadata: map[string]string{
"projectPath": serviceConfig.Path(),
"framework": "go",
},
},
},
}, nil
}

// Build compiles the Go project, cross-compiling for linux/amd64.
func (gp *goProject) Build(
ctx context.Context,
serviceConfig *ServiceConfig,
serviceContext *ServiceContext,
progress *async.Progress[ServiceProgress],
) (*ServiceBuildResult, error) {
progress.SetProgress(NewServiceProgress("Compiling Go project"))

buildDir, err := os.MkdirTemp("", "azd-go-build")
if err != nil {
return nil, fmt.Errorf("creating build directory: %w", err)
}

outputPath := filepath.Join(buildDir, goBinaryName)

// Cross-compile for linux/amd64 (Azure Functions target)
buildEnv := append(
gp.env.Environ(),
"GOOS=linux",
"GOARCH=amd64",
"CGO_ENABLED=0",
)

if err := gp.goCli.Build(
ctx, serviceConfig.Path(), outputPath, buildEnv,
); err != nil {
os.RemoveAll(buildDir)
return nil, fmt.Errorf("compiling Go project: %w", err)
}

return &ServiceBuildResult{
Artifacts: ArtifactCollection{
{
Kind: ArtifactKindDirectory,
Location: buildDir,
LocationKind: LocationKindLocal,
Metadata: map[string]string{
"buildPath": buildDir,
"binaryPath": outputPath,
"framework": "go",
"targetOS": "linux",
"targetArch": "amd64",
"buildOS": runtime.GOOS,
"buildArch": runtime.GOARCH,
},
},
},
}, nil
}

// Package stages the compiled binary and host.json into a deployment directory
// suitable for Azure Functions zip deploy.
// On Flex Consumption with runtime 'go', the platform provides the worker.config.json
// and proxy binary — the deployment package only needs the app binary and host.json.
func (gp *goProject) Package(
Comment thread
vhvb1989 marked this conversation as resolved.
ctx context.Context,
serviceConfig *ServiceConfig,
serviceContext *ServiceContext,
progress *async.Progress[ServiceProgress],
) (*ServicePackageResult, error) {
progress.SetProgress(NewServiceProgress("Staging Go Functions deployment"))

// Resolve build output directory and binary path
buildDir := ""
binaryRelPath := goBinaryName
if artifact, found := serviceContext.Build.FindFirst(
WithKind(ArtifactKindDirectory),
); found {
buildDir = artifact.Location
if bp, ok := artifact.Metadata["binaryPath"]; ok && bp != "" {
if filepath.IsAbs(bp) {
rel, err := filepath.Rel(buildDir, bp)
if err != nil || strings.HasPrefix(rel, "..") {
return nil, fmt.Errorf("binaryPath %q is not under build directory %q", bp, buildDir)
}
binaryRelPath = rel
} else {
// Validate relative path doesn't escape the build directory
cleaned := filepath.Clean(bp)
if strings.HasPrefix(cleaned, "..") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a shared helper for logic like this? Feels like someone skipping the .Clean could have things go poorly.

return nil, fmt.Errorf("binaryPath %q escapes the build directory", bp)
}
binaryRelPath = cleaned
}
}
}
if buildDir == "" {
return nil, fmt.Errorf("no build output found in service context")
}

packageDir, err := os.MkdirTemp("", "azd-go-package")
if err != nil {
return nil, fmt.Errorf("creating package directory: %w", err)
}

// Copy compiled binary and ensure execute permission is set.
// On Windows, os.Chmod is a no-op for Unix execute bits, but this is handled
// by rzip which defaults to 0755 on Windows when creating zip entries.
progress.SetProgress(NewServiceProgress("Copying compiled binary"))
binaryPath := filepath.Join(buildDir, binaryRelPath)
destBinaryPath := filepath.Join(packageDir, filepath.Base(binaryRelPath))
if err := copy.Copy(binaryPath, destBinaryPath); err != nil {
return nil, fmt.Errorf("copying Go binary: %w", err)
}
if err := os.Chmod(destBinaryPath, 0755); err != nil {
return nil, fmt.Errorf("setting binary permissions: %w", err)
}
Comment thread
vhvb1989 marked this conversation as resolved.

// Copy host.json from user project (required for Azure Functions deployment)
hostJSONSrc := filepath.Join(serviceConfig.Path(), "host.json")
if _, err := os.Stat(hostJSONSrc); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(
"host.json not found at %q: Azure Functions requires a host.json file in the project directory",
hostJSONSrc,
)
}
return nil, fmt.Errorf("checking host.json at %q: %w", hostJSONSrc, err)
}
Comment thread
Copilot marked this conversation as resolved.
if err := copy.Copy(
hostJSONSrc, filepath.Join(packageDir, "host.json"),
); err != nil {
return nil, fmt.Errorf("copying host.json: %w", err)
}

return &ServicePackageResult{
Artifacts: ArtifactCollection{
{
Kind: ArtifactKindDirectory,
Location: packageDir,
LocationKind: LocationKindLocal,
Metadata: map[string]string{
"packagePath": packageDir,
"framework": "go",
"host": "azure-function",
},
},
},
}, nil
}
Loading
Loading