Skip to content

Commit

Permalink
Support for nested Bicep infra modules with container app target (Azu…
Browse files Browse the repository at this point in the history
…re#117)

This fix ensure that the full infra module path is created when generating bicep module parameters file.

This fix allows the use of paths in the service moduleName property. We should also consider renaming this prop to modulePath or similar.
  • Loading branch information
wbreza authored Jul 22, 2022
1 parent 8273bf0 commit 53d3641
Show file tree
Hide file tree
Showing 14 changed files with 78 additions and 39 deletions.
2 changes: 2 additions & 0 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Breaking Changes

- [[#117]](https://github.com/Azure/azure-dev/issues/117) When specifying a custom module within a service the configuration key has been changed from `moduleName` to `module` and accepts a relative path to the infra module.

### Bugs Fixed

- [[#77]](https://github.com/Azure/azure-dev/issues/77) Use the correct command to log into the GitHub CLI in error messages. Thanks to community member [@TheEskhaton](https://github.com/TheEskhaton) for the fix!
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/cmd/infra_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/iac/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/infra"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/spin"
Expand Down Expand Up @@ -93,7 +94,7 @@ func (ica *infraCreateAction) Run(ctx context.Context, cmd *cobra.Command, args
if err != nil {
return fmt.Errorf("substituting parameter file: %w", err)
}
err = ioutil.WriteFile(azdCtx.BicepParametersFilePath(ica.rootOptions.EnvironmentName, rootModule), []byte(replaced), 0644)
err = ioutil.WriteFile(azdCtx.BicepParametersFilePath(ica.rootOptions.EnvironmentName, rootModule), []byte(replaced), osutil.PermissionFile)
if err != nil {
return fmt.Errorf("writing parameter file: %w", err)
}
Expand Down
10 changes: 2 additions & 8 deletions cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ type initAction struct {
rootOptions *commands.GlobalCommandOptions
}

//constant enums for file mode
const (
permissionDirectory = 0755
permissionRegularFile = 0644
)

func (i *initAction) SetupFlags(
persis *pflag.FlagSet,
local *pflag.FlagSet,
Expand Down Expand Up @@ -230,13 +224,13 @@ func (i *initAction) Run(ctx context.Context, _ *cobra.Command, args []string, a
}

//create .azure when running azd init
err = os.MkdirAll(filepath.Join(azdCtx.ProjectDirectory(), environment.EnvironmentDirectoryName), permissionDirectory)
err = os.MkdirAll(filepath.Join(azdCtx.ProjectDirectory(), environment.EnvironmentDirectoryName), osutil.PermissionDirectory)
if err != nil {
return fmt.Errorf("failed to create a directory: %w", err)
}

//create .gitignore or open existing .gitignore file, and contains .azure
gitignoreFile, err := os.OpenFile(filepath.Join(azdCtx.ProjectDirectory(), ".gitignore"), os.O_APPEND|os.O_RDWR|os.O_CREATE, permissionRegularFile)
gitignoreFile, err := os.OpenFile(filepath.Join(azdCtx.ProjectDirectory(), ".gitignore"), os.O_APPEND|os.O_RDWR|os.O_CREATE, osutil.PermissionFile)
if err != nil {
return fmt.Errorf("fail to create or open .gitignore: %w", err)
}
Expand Down
12 changes: 3 additions & 9 deletions cli/azd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/azure/azure-dev/cli/azd/cmd"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/blang/semver/v4"
"github.com/fatih/color"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -67,13 +68,6 @@ func main() {
}
}

// azdDirectoryPermissions are the permissions to use on the `.azd` folder in the user's home
// directory.
const azdDirectoryPermissions = 0755

// updateCheckFilePermissions are the permissions to use on the `update-check.json` file.
const updateCheckFilePermissions = 0644

// azdConfigDir is the name of the folder where `azd` writes user wide configuration data.
const azdConfigDir = ".azd"

Expand Down Expand Up @@ -183,7 +177,7 @@ func fetchLatestVersion(version chan<- semver.Version) {
// eagerly, since we have not yet sent the latest versions across the channel (and we don't want to do that until we've updated
// the cache since reader on the other end of the channel will exit the process after it receives this value and finishes
// the up to date check, possibly while this go-routine is still running)
if err := os.MkdirAll(filepath.Dir(cacheFilePath), azdDirectoryPermissions); err != nil {
if err := os.MkdirAll(filepath.Dir(cacheFilePath), osutil.PermissionFile); err != nil {
log.Printf("failed to create cache folder '%s': %v", filepath.Dir(cacheFilePath), err)
} else {
cacheObject := updateCacheFile{
Expand All @@ -194,7 +188,7 @@ func fetchLatestVersion(version chan<- semver.Version) {
// The marshal call can not fail, so we ignore the error.
cacheContents, _ := json.Marshal(cacheObject)

if err := os.WriteFile(cacheFilePath, cacheContents, updateCheckFilePermissions); err != nil {
if err := os.WriteFile(cacheFilePath, cacheContents, osutil.PermissionDirectory); err != nil {
log.Printf("failed to write update cache file: %v", err)
} else {
log.Printf("updated cache file to version %s (expires on: %s)", cacheObject.Version, cacheObject.ExpiresOn)
Expand Down
10 changes: 6 additions & 4 deletions cli/azd/pkg/environment/azd_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"path/filepath"
"sort"

"github.com/azure/azure-dev/cli/azd/pkg/osutil"
)

const ProjectFileName = "azure.yaml"
Expand Down Expand Up @@ -120,7 +122,7 @@ func (c *AzdContext) WriteBicepParameters(env string, module string, parameters
return fmt.Errorf("marshaling parameters: %w", err)
}

err = ioutil.WriteFile(c.BicepParametersFilePath(env, module), byts, 0644)
err = ioutil.WriteFile(c.BicepParametersFilePath(env, module), byts, osutil.PermissionFile)
if err != nil {
return fmt.Errorf("writing parameters file: %w", err)
}
Expand Down Expand Up @@ -195,7 +197,7 @@ func (c *AzdContext) SetDefaultEnvironmentName(name string) error {
return fmt.Errorf("serializing config file: %w", err)
}

if err := ioutil.WriteFile(path, byts, 0644); err != nil {
if err := ioutil.WriteFile(path, byts, osutil.PermissionFile); err != nil {
return fmt.Errorf("writing config file: %w", err)
}

Expand All @@ -205,11 +207,11 @@ func (c *AzdContext) SetDefaultEnvironmentName(name string) error {
var ErrEnvironmentExists = errors.New("environment already exists")

func (c *AzdContext) NewEnvironment(name string) error {
if err := os.MkdirAll(c.EnvironmentDirectory(), 0755); err != nil {
if err := os.MkdirAll(c.EnvironmentDirectory(), osutil.PermissionDirectory); err != nil {
return fmt.Errorf("creating environment root: %w", err)
}

if err := os.Mkdir(filepath.Join(c.EnvironmentDirectory(), name), 0755); err != nil {
if err := os.Mkdir(filepath.Join(c.EnvironmentDirectory(), name), osutil.PermissionDirectory); err != nil {
if errors.Is(err, os.ErrExist) {
return ErrEnvironmentExists
}
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"regexp"

"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/joho/godotenv"
)

Expand Down Expand Up @@ -84,7 +85,7 @@ func (e *Environment) Save() error {
return nil
}

err := os.MkdirAll(filepath.Dir(e.File), 0755)
err := os.MkdirAll(filepath.Dir(e.File), osutil.PermissionDirectory)
if err != nil {
return fmt.Errorf("failed to create a directory: %w", err)
}
Expand Down
8 changes: 8 additions & 0 deletions cli/azd/pkg/osutil/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package osutil

import "os"

const (
PermissionDirectory os.FileMode = 0755
PermissionFile os.FileMode = 0644
)
2 changes: 1 addition & 1 deletion cli/azd/pkg/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func NewProject(path string, name string) (*Project, error) {
return nil, fmt.Errorf("preparing new project file contents: %w", err)
}

err = os.WriteFile(path, projectFileContents.Bytes(), 0644)
err = os.WriteFile(path, projectFileContents.Bytes(), osutil.PermissionFile)
if err != nil {
return nil, fmt.Errorf("writing project file: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/pkg/project/project_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ func ParseProjectConfig(yamlContent string, env *environment.Environment) (*Proj
svc.Project = &projectFile

// By convention, the name of the infrastructure module to use when doing an IaC based deployment is the friendly
// name of the service. This may be overridden by the `moduleName` property of `azure.yaml`
if svc.ModuleName == "" {
svc.ModuleName = key
// name of the service. This may be overridden by the `module` property of `azure.yaml`
if svc.Module == "" {
svc.Module = key
}
}

Expand Down
28 changes: 27 additions & 1 deletion cli/azd/pkg/project/project_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ services:
require.Equal(t, 2, len(projectConfig.Services))

for key, svc := range projectConfig.Services {
require.Equal(t, key, svc.ModuleName)
require.Equal(t, key, svc.Module)
require.Equal(t, key, svc.Name)
require.Equal(t, projectConfig, svc.Project)
}
Expand Down Expand Up @@ -138,3 +138,29 @@ services:
require.Equal(t, "./Dockerfile.dev", service.Docker.Path)
require.Equal(t, "../", service.Docker.Context)
}

func TestProjectWithCustomModule(t *testing.T) {
const testProj = `
name: test-proj
metadata:
template: test-proj-template
services:
api:
project: src/api
language: js
host: containerapp
module: ./api/api
`

e := environment.Environment{Values: make(map[string]string)}
e.SetEnvName("test-env")

projectConfig, err := ParseProjectConfig(testProj, &e)

require.NotNil(t, projectConfig)
require.Nil(t, err)

service := projectConfig.Services["api"]

require.Equal(t, "./api/api", service.Module)
}
4 changes: 2 additions & 2 deletions cli/azd/pkg/project/service_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ type ServiceConfig struct {
Language string `yaml:"language"`
// The output path for build artifacts
OutputPath string `yaml:"dist"`
// The infrastructure module name to use for this project
ModuleName string `yaml:"moduleName"`
// The infrastructure module path relative to the root infra folder to use for this project
Module string `yaml:"module"`
// The optional docker options
Docker DockerProjectOptions `yaml:"docker"`
}
Expand Down
20 changes: 15 additions & 5 deletions cli/azd/pkg/project/service_target_containerapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"

"github.com/azure/azure-dev/cli/azd/pkg/azure"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/iac/bicep"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/drone/envsubst"
)
Expand All @@ -32,7 +34,7 @@ func (at *containerAppTarget) RequiredExternalTools() []tools.ExternalTool {
}

func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.AzdContext, path string, progress chan<- string) (ServiceDeploymentResult, error) {
bicepPath := azdCtx.BicepModulePath(at.config.ModuleName)
bicepPath := azdCtx.BicepModulePath(at.config.Module)

progress <- "Creating deployment template"
template, err := bicep.Compile(ctx, tools.NewBicepCli(at.cli), bicepPath)
Expand Down Expand Up @@ -82,7 +84,7 @@ func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
log.Print("generating deployment parameters file")

// Copy the parameter template file to the environment working directory and do substitutions.
parametersTemplate := azdCtx.BicepParametersTemplateFilePath(at.config.ModuleName)
parametersTemplate := azdCtx.BicepParametersTemplateFilePath(at.config.Module)
templateBytes, err := ioutil.ReadFile(parametersTemplate)
if err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("reading parameter file template: %w", err)
Expand All @@ -98,8 +100,16 @@ func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
return ServiceDeploymentResult{}, fmt.Errorf("substituting parameter file: %w", err)
}

parametersFile := azdCtx.BicepParametersFilePath(at.env.GetEnvName(), at.config.ModuleName)
err = ioutil.WriteFile(parametersFile, []byte(replaced), 0644)
parametersFile := azdCtx.BicepParametersFilePath(at.env.GetEnvName(), at.config.Module)

// If the bicep uses nested modules ensure the full directory tree
// is created before copying the parameters file.
directoryPath := filepath.Dir(parametersFile)
if err := os.MkdirAll(directoryPath, osutil.PermissionDirectory); err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("creating directory tree: %w", err)
}

err = ioutil.WriteFile(parametersFile, []byte(replaced), osutil.PermissionFile)
if err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("writing parameter file: %w", err)
}
Expand All @@ -109,7 +119,7 @@ func (at *containerAppTarget) Deploy(ctx context.Context, azdCtx *environment.Az
deploymentTarget := bicep.NewResourceGroupDeploymentTarget(at.cli, at.env.GetSubscriptionId(), at.scope.ResourceGroupName(), at.scope.ResourceName())

progress <- "Updating container app image reference"
res, err := bicep.Deploy(ctx, deploymentTarget, azdCtx.BicepModulePath(at.config.ModuleName), parametersFile)
res, err := bicep.Deploy(ctx, deploymentTarget, azdCtx.BicepModulePath(at.config.Module), parametersFile)
if err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("updating infrastructure: %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions cli/azd/test/functional/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/test/azdcli"
"github.com/joho/godotenv"
Expand Down Expand Up @@ -430,14 +431,14 @@ func copySample(targetRoot string, sampleName string) error {
targetPath := filepath.Join(targetRoot, name[len(sampleRoot):])

if info.IsDir() {
return os.MkdirAll(targetPath, 0755)
return os.MkdirAll(targetPath, osutil.PermissionDirectory)
}

contents, err := ioutil.ReadFile(name)
if err != nil {
return fmt.Errorf("reading sample file: %w", err)
}
return ioutil.WriteFile(targetPath, contents, 0644)
return ioutil.WriteFile(targetPath, contents, osutil.PermissionFile)
})
}

Expand Down
4 changes: 2 additions & 2 deletions schemas/v1.0/azure.yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@
"ts"
]
},
"moduleName": {
"module": {
"type": "string",
"title": "Name of the module used to deploy the service",
"title": "Path of the infrastructure module used to deploy the service relative to the root infra folder",
"description": "If omitted, the CLI will assume the module name is the same as the service name."
},
"dist": {
Expand Down

0 comments on commit 53d3641

Please sign in to comment.