Skip to content

Commit

Permalink
azd package support for user specified output paths (#2721)
Browse files Browse the repository at this point in the history
Adds ability for a user to specify a --output-path param to the azd package command to control the output location of file based packages.

For top level commands it would be expected that user specify a absolute or relative folder path. If the path does not exist azd will create it automatically.
  • Loading branch information
wbreza committed Sep 8, 2023
1 parent 4cd286f commit eee777a
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 8 deletions.
2 changes: 1 addition & 1 deletion cli/azd/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (da *deployAction) Run(ctx context.Context) (*actions.ActionResult, error)
}
} else {
// --from-package not set, package the application
packageTask := da.serviceManager.Package(ctx, svc, nil)
packageTask := da.serviceManager.Package(ctx, svc, nil, nil)
done := make(chan struct{})
go func() {
for packageProgress := range packageTask.Progress() {
Expand Down
16 changes: 15 additions & 1 deletion cli/azd/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type packageFlags struct {
all bool
global *internal.GlobalCommandOptions
*envFlag
outputPath string
}

func newPackageFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *packageFlags {
Expand All @@ -43,6 +44,12 @@ func (pf *packageFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComman
false,
"Deploys all services that are listed in "+azdcontext.ProjectFileName,
)
local.StringVar(
&pf.outputPath,
"output-path",
"",
"File or folder path where the generated packages will be saved.",
)
}

func newPackageCmd() *cobra.Command {
Expand Down Expand Up @@ -146,7 +153,8 @@ func (pa *packageAction) Run(ctx context.Context) (*actions.ActionResult, error)
continue
}

packageTask := pa.serviceManager.Package(ctx, svc, nil)
options := &project.PackageOptions{OutputPath: pa.flags.outputPath}
packageTask := pa.serviceManager.Package(ctx, svc, nil, options)
done := make(chan struct{})
go func() {
for packageProgress := range packageTask.Progress() {
Expand Down Expand Up @@ -210,5 +218,11 @@ func getCmdPackageHelpFooter(*cobra.Command) string {
"Packages all services in the current project to Azure.": output.WithHighLightFormat("azd package --all"),
"Packages the service named 'api' to Azure.": output.WithHighLightFormat("azd package api"),
"Packages the service named 'web' to Azure.": output.WithHighLightFormat("azd package web"),
"Packages all services to the specified output path.": output.WithHighLightFormat(
"azd package --output-path ./dist",
),
"Packages the service named 'api' to the specified output path.": output.WithHighLightFormat(
"azd package api --output-path ./dist/api.zip",
),
})
}
7 changes: 7 additions & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-package.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Flags
--docs : Opens the documentation for azd package in your web browser.
-e, --environment string : The name of the environment to use.
-h, --help : Gets help for package.
--output-path string : File or folder path where the generated packages will be saved.

Global Flags
-C, --cwd string : Sets the current working directory.
Expand All @@ -23,9 +24,15 @@ Examples
Packages all services in the current project to Azure.
azd package --all

Packages all services to the specified output path.
azd package --output-path ./dist

Packages the service named 'api' to Azure.
azd package api

Packages the service named 'api' to the specified output path.
azd package api --output-path ./dist/api.zip

Packages the service named 'web' to Azure.
azd package web

Expand Down
7 changes: 5 additions & 2 deletions cli/azd/pkg/project/project_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ package project
import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/azure/azure-dev/cli/azd/pkg/rzip"
"github.com/otiai10/copy"
)

// CreateDeployableZip creates a zip file of a folder, recursively.
// Returns the path to the created zip file or an error if it fails.
func createDeployableZip(appName string, path string) (string, error) {
func createDeployableZip(projectName string, appName string, path string) (string, error) {
// TODO: should probably avoid picking up files that weren't meant to be deployed (ie, local .env files, etc..)
zipFile, err := os.CreateTemp("", "azddeploy*.zip")
filePath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-azddeploy-%d.zip", projectName, appName, time.Now().Unix()))
zipFile, err := os.Create(filePath)
if err != nil {
return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err)
}
Expand Down
78 changes: 78 additions & 0 deletions cli/azd/pkg/project/service_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ package project
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"

"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"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/ext"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
)

Expand Down Expand Up @@ -68,6 +73,7 @@ type ServiceManager interface {
ctx context.Context,
serviceConfig *ServiceConfig,
buildOutput *ServiceBuildResult,
options *PackageOptions,
) *async.TaskWithProgress[*ServicePackageResult, ServiceProgress]

// Deploys the generated artifacts to the Azure resource that will
Expand Down Expand Up @@ -256,8 +262,13 @@ func (sm *serviceManager) Package(
ctx context.Context,
serviceConfig *ServiceConfig,
buildOutput *ServiceBuildResult,
options *PackageOptions,
) *async.TaskWithProgress[*ServicePackageResult, ServiceProgress] {
return async.RunTaskWithProgress(func(task *async.TaskContextWithProgress[*ServicePackageResult, ServiceProgress]) {
if options == nil {
options = &PackageOptions{}
}

cachedResult, ok := sm.getOperationResult(ctx, serviceConfig, string(ServiceEventPackage))
if ok && cachedResult != nil {
task.SetResult(cachedResult.(*ServicePackageResult))
Expand Down Expand Up @@ -361,6 +372,45 @@ func (sm *serviceManager) Package(
return
}

// Package path can be a file path or a container image name
// We only move to desired output path for file based packages
_, err = os.Stat(packageResult.PackagePath)
hasPackageFile := err == nil

if hasPackageFile && options.OutputPath != "" {
var destFilePath string
var destDirectory string

isFilePath := filepath.Ext(options.OutputPath) != ""
if isFilePath {
destFilePath = options.OutputPath
destDirectory = filepath.Dir(options.OutputPath)
} else {
destFilePath = filepath.Join(options.OutputPath, filepath.Base(packageResult.PackagePath))
destDirectory = options.OutputPath
}

_, err := os.Stat(destDirectory)
if errors.Is(err, os.ErrNotExist) {
// Create the desired output directory if it does not already exist
if err := os.MkdirAll(destDirectory, osutil.PermissionDirectory); err != nil {
task.SetError(fmt.Errorf("failed creating output directory '%s': %w", destDirectory, err))
return
}
}

// Move the package file to the desired path
// We can't use os.Rename here since that does not work across disks
if err := moveFile(packageResult.PackagePath, destFilePath); err != nil {
task.SetError(
fmt.Errorf("failed moving package file '%s' to '%s': %w", packageResult.PackagePath, destFilePath, err),
)
return
}

packageResult.PackagePath = destFilePath
}

task.SetResult(packageResult)
})
}
Expand Down Expand Up @@ -569,3 +619,31 @@ func syncProgress[T comparable, P comparable](task *async.TaskContextWithProgres
task.SetProgress(progress)
}
}

// Copies a file from the source path to the destination path
// Deletes the source file after the copy is complete
func moveFile(sourcePath string, destinationPath string) error {
sourceFile, err := os.Open(sourcePath)
if err != nil {
return fmt.Errorf("opening source file: %w", err)
}
defer sourceFile.Close()

// Create or truncate the destination file
destinationFile, err := os.Create(destinationPath)
if err != nil {
return fmt.Errorf("creating destination file: %w", err)
}
defer destinationFile.Close()

// Copy the contents of the source file to the destination file
_, err = io.Copy(destinationFile, sourceFile)
if err != nil {
return fmt.Errorf("copying file: %w", err)
}

// Remove the source file (optional)
defer os.Remove(sourcePath)

return nil
}
4 changes: 2 additions & 2 deletions cli/azd/pkg/project/service_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func Test_ServiceManager_Package(t *testing.T) {
ctx := context.WithValue(*mockContext.Context, frameworkPackageCalled, fakeFrameworkPackageCalled)
ctx = context.WithValue(ctx, serviceTargetPackageCalled, fakeServiceTargetPackageCalled)

packageTask := sm.Package(ctx, serviceConfig, nil)
packageTask := sm.Package(ctx, serviceConfig, nil, nil)
logProgress(packageTask)

result, err := packageTask.Await()
Expand Down Expand Up @@ -296,7 +296,7 @@ func Test_ServiceManager_Events_With_Errors(t *testing.T) {
{
name: "package",
run: func(ctx context.Context, serviceManager ServiceManager, serviceConfig *ServiceConfig) (any, error) {
packageTask := serviceManager.Package(ctx, serviceConfig, nil)
packageTask := serviceManager.Package(ctx, serviceConfig, nil, nil)
logProgress(packageTask)
return packageTask.Await()
},
Expand Down
4 changes: 4 additions & 0 deletions cli/azd/pkg/project/service_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (sbr *ServiceBuildResult) MarshalJSON() ([]byte, error) {
return json.Marshal(*sbr)
}

type PackageOptions struct {
OutputPath string
}

// ServicePackageResult is the result of a successful Package operation
type ServicePackageResult struct {
Build *ServiceBuildResult `json:"build"`
Expand Down
6 changes: 5 additions & 1 deletion cli/azd/pkg/project/service_target_appservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ func (st *appServiceTarget) Package(
return async.RunTaskWithProgress(
func(task *async.TaskContextWithProgress[*ServicePackageResult, ServiceProgress]) {
task.SetProgress(NewServiceProgress("Compressing deployment artifacts"))
zipFilePath, err := createDeployableZip(serviceConfig.Name, packageOutput.PackagePath)
zipFilePath, err := createDeployableZip(
serviceConfig.Project.Name,
serviceConfig.Name,
packageOutput.PackagePath,
)
if err != nil {
task.SetError(err)
return
Expand Down
6 changes: 5 additions & 1 deletion cli/azd/pkg/project/service_target_functionapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ func (f *functionAppTarget) Package(
return async.RunTaskWithProgress(
func(task *async.TaskContextWithProgress[*ServicePackageResult, ServiceProgress]) {
task.SetProgress(NewServiceProgress("Compressing deployment artifacts"))
zipFilePath, err := createDeployableZip(serviceConfig.Name, packageOutput.PackagePath)
zipFilePath, err := createDeployableZip(
serviceConfig.Project.Name,
serviceConfig.Name,
packageOutput.PackagePath,
)
if err != nil {
task.SetError(err)
return
Expand Down

0 comments on commit eee777a

Please sign in to comment.