Skip to content
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/cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func loadOrInitEnvironment(
return nil, false, err
}

return environment.EmptyWithFile(azdCtx.GetEnvironmentFilePath(*environmentName)), true, nil
return environment.EmptyWithRoot(azdCtx.EnvironmentRoot(*environmentName)), true, nil
}

env, isNew, err := loadOrCreateEnvironment()
Expand Down
12 changes: 12 additions & 0 deletions cli/azd/pkg/azure/arm_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ package azure

// ArmTemplate is a JSON encoded ARM template.
type ArmTemplate string

type ArmParameters map[string]ArmParameterValue

type ArmParameterFile struct {
Schema string `json:"$schema"`
ContentVersion string `json:"contentVersion"`
Parameters ArmParameters `json:"parameters"`
}

type ArmParameterValue struct {
Value any `json:"value"`
}
8 changes: 4 additions & 4 deletions cli/azd/pkg/commands/pipeline/azdo_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package pipeline
import (
"context"
"errors"
"path"
"path/filepath"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/azdo"
Expand Down Expand Up @@ -160,12 +160,12 @@ func Test_saveEnvironmentConfig(t *testing.T) {
key := "test"
value := "12345"
provider := getEmptyAzdoScmProviderTestHarness()
envPath := path.Join(tempDir, ".test.env")
provider.Env = environment.EmptyWithFile(envPath)
envPath := filepath.Join(tempDir, "test")
provider.Env = environment.EmptyWithRoot(envPath)
// act
e := provider.saveEnvironmentConfig(key, value)
// assert
writtenEnv, err := environment.FromFile(envPath)
writtenEnv, err := environment.FromRoot(envPath)
require.NoError(t, err)

require.EqualValues(t, writtenEnv.Values[key], value)
Expand Down
13 changes: 9 additions & 4 deletions cli/azd/pkg/environment/azdcontext/azdcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

const ProjectFileName = "azure.yaml"
const EnvironmentDirectoryName = ".azure"
const DotEnvFileName = ".env"
const ConfigFileName = "config.json"
const ConfigFileVersion = 1
const InfraDirectoryName = "infra"
Expand Down Expand Up @@ -46,12 +47,16 @@ func (c *AzdContext) GetDefaultProjectName() string {
return filepath.Base(c.ProjectDirectory())
}

func (c *AzdContext) GetEnvironmentFilePath(name string) string {
return filepath.Join(c.EnvironmentDirectory(), name, ".env")
func (c *AzdContext) EnvironmentDotEnvPath(name string) string {
return filepath.Join(c.EnvironmentDirectory(), name, DotEnvFileName)
}

func (c *AzdContext) EnvironmentRoot(name string) string {
return filepath.Join(c.EnvironmentDirectory(), name)
}

func (c *AzdContext) GetEnvironmentWorkDirectory(name string) string {
return filepath.Join(c.GetEnvironmentFilePath(name), "wd")
return filepath.Join(c.EnvironmentRoot(name), "wd")
}

func (c *AzdContext) GetInfrastructurePath() string {
Expand All @@ -75,7 +80,7 @@ func (c *AzdContext) ListEnvironments() ([]contracts.EnvListEnvironment, error)
ev := contracts.EnvListEnvironment{
Name: ent.Name(),
IsDefault: ent.Name() == defaultEnv,
DotEnvPath: c.GetEnvironmentFilePath(ent.Name()),
DotEnvPath: c.EnvironmentDotEnvPath(ent.Name()),
}
envs = append(envs, ev)
}
Expand Down
66 changes: 44 additions & 22 deletions 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/config"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/joho/godotenv"
Expand Down Expand Up @@ -39,10 +40,14 @@ const ResourceGroupEnvVarName = "AZURE_RESOURCE_GROUP"
type Environment struct {
// Values is a map of setting names to values.
Values map[string]string
// File is a path to the file that backs this environment. If empty, the Environment

// Config is environment specific config
Config config.Config

// File is a path to the directory that backs this environment. If empty, the Environment
// will not be persisted when `Save` is called. This allows the zero value to be used
// for testing.
File string
Root string
}

// Same restrictions as a deployment name (ref:
Expand All @@ -53,41 +58,51 @@ func IsValidEnvironmentName(name string) bool {
return environmentNameRegexp.MatchString(name)
}

// FromFile loads an environment from a file on disk. On error,
// FromRoot loads an environment located in a directory. On error,
// an valid empty environment file, configured to persist its contents
// to file, is returned.
func FromFile(file string) (*Environment, error) {
// to this directory, is returned.
func FromRoot(root string) (*Environment, error) {
env := &Environment{
File: file,
Values: make(map[string]string),
Root: root,
}

e, err := godotenv.Read(file)
envPath := filepath.Join(root, azdcontext.DotEnvFileName)
e, err := godotenv.Read(envPath)
if err != nil {
env.Values = make(map[string]string)
return env, fmt.Errorf("can't read %s: %w", file, err)
return EmptyWithRoot(root), fmt.Errorf("loading .env: %w", err)
}

env.Values = e

cfgPath := filepath.Join(root, azdcontext.ConfigFileName)

cfgMgr := config.NewManager()
cfg, err := cfgMgr.Load(cfgPath)
if err != nil {
return EmptyWithRoot(root), fmt.Errorf("loading config: %w", err)
}
env.Config = cfg

return env, nil
}

func GetEnvironment(azdContext *azdcontext.AzdContext, name string) (*Environment, error) {
return FromFile(azdContext.GetEnvironmentFilePath(name))
return FromRoot(azdContext.EnvironmentRoot(name))
}

// EmptyWithFile returns an empty environment, which will be persisted
// to a given file when saved.
func EmptyWithFile(file string) *Environment {
// EmptyWithRoot returns an empty environment, which will be persisted
// to a given directory when saved.
func EmptyWithRoot(root string) *Environment {
return &Environment{
File: file,
Root: root,
Values: make(map[string]string),
Config: config.NewConfig(nil),
}
}

func Ephemeral() *Environment {
return &Environment{
Values: make(map[string]string),
Config: config.NewConfig(nil),
}
}

Expand All @@ -107,21 +122,28 @@ func EphemeralWithValues(name string, values map[string]string) *Environment {
return env
}

// If `File` is set, Save writes the current contents of the environment to
// the given file, creating it and any intermediate directories as needed.
// If `Root` is set, Save writes the current contents of the environment to
// the given directory, creating it and any intermediate directories as needed.
func (e *Environment) Save() error {
if e.File == "" {
if e.Root == "" {
return nil
}

err := os.MkdirAll(filepath.Dir(e.File), osutil.PermissionDirectory)
err := os.MkdirAll(e.Root, osutil.PermissionDirectory)
if err != nil {
return fmt.Errorf("failed to create a directory: %w", err)
}

err = godotenv.Write(e.Values, e.File)
err = godotenv.Write(e.Values, filepath.Join(e.Root, azdcontext.DotEnvFileName))
if err != nil {
return fmt.Errorf("saving .env: %w", err)
}

cfgMgr := config.NewManager()

err = cfgMgr.Save(e.Config, filepath.Join(e.Root, azdcontext.ConfigFileName))
if err != nil {
return fmt.Errorf("can't write '%s': %w", e.File, err)
return fmt.Errorf("saving config: %w", err)
}

return nil
Expand Down
31 changes: 31 additions & 0 deletions cli/azd/pkg/environment/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
package environment

import (
"errors"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestIsValidEnvironmentName(t *testing.T) {
Expand All @@ -19,3 +22,31 @@ func TestIsValidEnvironmentName(t *testing.T) {
assert.False(t, IsValidEnvironmentName("no spaces"))
assert.False(t, IsValidEnvironmentName("12345678901234567890123456789012345678901234567890123456789012345"))
}

func TestConfigRoundTrips(t *testing.T) {
root := t.TempDir()

// Create a new config from an empty root. We expect this to fail (because there is no configuration data), but
// to get back an empty configuration object we can use regardless.
e, err := FromRoot(root)
require.Error(t, err)
require.True(t, errors.Is(err, os.ErrNotExist))

// There should be no configuration since this is an empty environment.
require.True(t, e.Config.IsEmpty())

// Set a config value.
err = e.Config.Set("is.this.a.test", true)
require.NoError(t, err)

// Save the environment
err = e.Save()
require.NoError(t, err)

// Load the environment back up, we expect no error and for the config value we wrote to still exist.
e, err = FromRoot(root)
require.NoError(t, err)
v, has := e.Config.Get("is.this.a.test")
require.True(t, has)
require.Equal(t, true, v)
}
Loading