-
Notifications
You must be signed in to change notification settings - Fork 317
Add Go Azure Functions framework service support #8599
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
base: main
Are you sure you want to change the base?
Changes from all commits
c0fb25a
a00b9b6
ac1f8e3
220c6c6
99c5b4a
383e32f
1b25bc3
80633dc
e20cf6d
eb02aa4
c11daaa
17a596a
1fe9bd5
fc2ffc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ words: | |
| - azdignore | ||
| - gofmt | ||
| - golangci | ||
| - golangtools | ||
| - lightspeed | ||
| - runewidth | ||
| - toplevel | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The existing |
||
| return ServiceLanguageGo, nil | ||
| } | ||
|
|
||
| switch kind { | ||
| case ServiceLanguageNone, | ||
|
|
@@ -43,6 +50,7 @@ func parseServiceLanguage(kind ServiceLanguageKind) (ServiceLanguageKind, error) | |
| ServiceLanguageTypeScript, | ||
| ServiceLanguagePython, | ||
| ServiceLanguageJava, | ||
| ServiceLanguageGo, | ||
| ServiceLanguageDocker, | ||
| ServiceLanguageCustom: | ||
| // Excluding ServiceLanguageSwa since it is implicitly derived currently, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
|
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" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ) | ||
|
|
||
| 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, | ||
| }, | ||
|
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 | ||
|
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( | ||
|
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, "..") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) | ||
| } | ||
|
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) | ||
| } | ||
|
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 | ||
| } | ||
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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.
pyfor python,golangfor go). The canonical constant names likejavascript/typescriptalready match typical usage so they don't need separate aliases. Fixed in 1fe9bd5.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 --