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
4 changes: 2 additions & 2 deletions cli/azd/cmd/middleware/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (m *HooksMiddleware) registerCommandHooks(
m.console,
projectConfig.Path,
projectConfig.Hooks,
env.Environ(),
env,
)

var actionResult *actions.ActionResult
Expand Down Expand Up @@ -142,7 +142,7 @@ func (m *HooksMiddleware) registerServiceHooks(
m.console,
service.Path(),
service.Hooks,
env.Environ(),
env,
)

for hookName, hookConfig := range service.Hooks {
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/pkg/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (c *manager) Save(config Config, filePath string) error {
return fmt.Errorf("failed marshalling config JSON: %w", err)
}

folderPath := filepath.Dir(filePath)
if err := os.MkdirAll(folderPath, osutil.PermissionDirectory); err != nil {
return fmt.Errorf("failed creating config directory: %w", err)
}

err = os.WriteFile(filePath, configJson, osutil.PermissionFile)
if err != nil {
return fmt.Errorf("failed writing configuration data: %w", err)
Expand Down
70 changes: 45 additions & 25 deletions cli/azd/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,24 +75,8 @@ func FromRoot(root string) (*Environment, error) {
Root: root,
}

envPath := filepath.Join(root, azdcontext.DotEnvFileName)
if e, err := godotenv.Read(envPath); errors.Is(err, os.ErrNotExist) {
env.Values = make(map[string]string)
} else if err != nil {
return EmptyWithRoot(root), fmt.Errorf("loading .env: %w", err)
} else {
env.Values = e
}

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

cfgMgr := config.NewManager()
if cfg, err := cfgMgr.Load(cfgPath); errors.Is(err, os.ErrNotExist) {
env.Config = config.NewConfig(nil)
} else if err != nil {
return EmptyWithRoot(root), fmt.Errorf("loading config: %w", err)
} else {
env.Config = cfg
if err := env.Reload(); err != nil {
return EmptyWithRoot(root), err
}

return env, nil
Expand Down Expand Up @@ -144,13 +128,56 @@ func (e *Environment) Getenv(key string) string {
return os.Getenv(key)
}

// Reloads environment variables and configuration
func (e *Environment) Reload() error {
// Reload env values
envPath := filepath.Join(e.Root, azdcontext.DotEnvFileName)
if envMap, err := godotenv.Read(envPath); errors.Is(err, os.ErrNotExist) {
e.Values = make(map[string]string)
} else if err != nil {
return fmt.Errorf("loading .env: %w", err)
} else {
e.Values = envMap
}

// Reload env config
cfgPath := filepath.Join(e.Root, azdcontext.ConfigFileName)
cfgMgr := config.NewManager()
if cfg, err := cfgMgr.Load(cfgPath); errors.Is(err, os.ErrNotExist) {
e.Config = config.NewConfig(nil)
} else if err != nil {
return fmt.Errorf("loading config: %w", err)
} else {
e.Config = cfg
}

return nil
}

// 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.Root == "" {
return nil
}

// Update configuration
cfgMgr := config.NewManager()
if err := cfgMgr.Save(e.Config, filepath.Join(e.Root, azdcontext.ConfigFileName)); err != nil {
return fmt.Errorf("saving config: %w", err)
}

// Cache current values & reload to get any new env vars
currentValues := e.Values
if err := e.Reload(); err != nil {
return fmt.Errorf("failed reloading env vars, %w", err)
}

// Overlay current values before saving
for key, value := range currentValues {
e.Values[key] = value
}

err := os.MkdirAll(e.Root, osutil.PermissionDirectory)
if err != nil {
return fmt.Errorf("failed to create a directory: %w", err)
Expand All @@ -161,13 +188,6 @@ func (e *Environment) Save() error {
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("saving config: %w", err)
}

return nil
}

Expand Down
41 changes: 41 additions & 0 deletions cli/azd/pkg/environment/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (
"path/filepath"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/test/ostest"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -106,3 +109,41 @@ func TestFromRoot(t *testing.T) {
require.True(t, e.Config.IsEmpty())
})
}

func Test_SaveAndReload(t *testing.T) {
tempDir := t.TempDir()
ostest.Chdir(t, tempDir)

env, err := FromRoot(tempDir)
require.NotNil(t, env)
require.NoError(t, err)

env.SetLocation("eastus2")
env.SetSubscriptionId("SUBSCRIPTION_ID")

err = env.Save()
require.NoError(t, err)

// Simulate another process updating the .env file
envPath := filepath.Join(tempDir, azdcontext.DotEnvFileName)
envMap, err := godotenv.Read(envPath)
require.NotNil(t, envMap)
require.NoError(t, err)

// This entry does not exist in the current env state but is added as part of the reload process
envMap["SERVICE_API_ENDPOINT_URL"] = "http://api.example.com"
err = godotenv.Write(envMap, envPath)
require.NoError(t, err)

// Set a new property in the env
env.SetServiceProperty("web", "ENDPOINT_URL", "http://web.example.com")
err = env.Save()
require.NoError(t, err)

// Verify all values exist with expected values
// All values now exist whether or not they were in the env state to start with
require.Equal(t, env.Values["SERVICE_WEB_ENDPOINT_URL"], "http://web.example.com")
require.Equal(t, env.Values["SERVICE_API_ENDPOINT_URL"], "http://api.example.com")
require.Equal(t, "SUBSCRIPTION_ID", env.GetSubscriptionId())
require.Equal(t, "eastus2", env.GetLocation())
}
16 changes: 11 additions & 5 deletions cli/azd/pkg/ext/hooks_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand All @@ -23,7 +24,7 @@ type HooksRunner struct {
console input.Console
cwd string
hooks map[string]*HookConfig
envVars []string
env *environment.Environment
}

// NewHooks creates a new instance of CommandHooks
Expand All @@ -34,7 +35,7 @@ func NewHooksRunner(
console input.Console,
cwd string,
hooks map[string]*HookConfig,
envVars []string,
env *environment.Environment,
) *HooksRunner {
if cwd == "" {
osWd, err := os.Getwd()
Expand All @@ -51,7 +52,7 @@ func NewHooksRunner(
console: console,
cwd: cwd,
hooks: hooks,
envVars: envVars,
env: env,
}
}

Expand Down Expand Up @@ -82,6 +83,11 @@ func (h *HooksRunner) RunHooks(ctx context.Context, hookType HookType, commands
return fmt.Errorf("failed running scripts for hooks '%s', %w", strings.Join(commands, ","), err)
}

// Reload env vars before execution to enable support for hooks to generate new env vars between commands
if err := h.env.Reload(); err != nil {
return fmt.Errorf("failed reloading env values, %w", err)
}

for _, hookConfig := range hooks {
err := h.execHook(ctx, hookConfig)
if err != nil {
Expand All @@ -101,9 +107,9 @@ func (h *HooksRunner) GetScript(hookConfig *HookConfig) (tools.Script, error) {

switch hookConfig.Shell {
case ShellTypeBash:
return bash.NewBashScript(h.commandRunner, h.cwd, h.envVars), nil
return bash.NewBashScript(h.commandRunner, h.cwd, h.env.Environ()), nil
case ShellTypePowershell:
return powershell.NewPowershellScript(h.commandRunner, h.cwd, h.envVars), nil
return powershell.NewPowershellScript(h.commandRunner, h.cwd, h.env.Environ()), nil
default:
return nil, fmt.Errorf(
"shell type '%s' is not a valid option. Only 'sh' and 'pwsh' are supported",
Expand Down
31 changes: 19 additions & 12 deletions cli/azd/pkg/ext/hooks_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/test/mocks"
Expand All @@ -18,10 +19,13 @@ func Test_Hooks_Execute(t *testing.T) {
cwd := t.TempDir()
ostest.Chdir(t, cwd)

env := []string{
"a=apple",
"b=banana",
}
env := environment.EphemeralWithValues(
"test",
map[string]string{
"a": "apple",
"b": "banana",
},
)

hooks := map[string]*HookConfig{
"preinline": {
Expand Down Expand Up @@ -56,7 +60,7 @@ func Test_Hooks_Execute(t *testing.T) {
ranPreHook = true
require.Equal(t, "scripts/precommand.sh", args.Args[0])
require.Equal(t, cwd, args.Cwd)
require.Equal(t, env, args.Env)
require.Equal(t, env.Environ(), args.Env)
require.Equal(t, false, args.Interactive)

return exec.NewRunResult(0, "", ""), nil
Expand All @@ -82,7 +86,7 @@ func Test_Hooks_Execute(t *testing.T) {
ranPostHook = true
require.Equal(t, "scripts/postcommand.sh", args.Args[0])
require.Equal(t, cwd, args.Cwd)
require.Equal(t, env, args.Env)
require.Equal(t, env.Environ(), args.Env)
require.Equal(t, false, args.Interactive)

return exec.NewRunResult(0, "", ""), nil
Expand All @@ -108,7 +112,7 @@ func Test_Hooks_Execute(t *testing.T) {
ranPostHook = true
require.Equal(t, "scripts/preinteractive.sh", args.Args[0])
require.Equal(t, cwd, args.Cwd)
require.Equal(t, env, args.Env)
require.Equal(t, env.Environ(), args.Env)
require.Equal(t, true, args.Interactive)

return exec.NewRunResult(0, "", ""), nil
Expand Down Expand Up @@ -200,10 +204,13 @@ func Test_Hooks_GetScript(t *testing.T) {
cwd := t.TempDir()
ostest.Chdir(t, cwd)

env := []string{
"a=apple",
"b=banana",
}
env := environment.EphemeralWithValues(
"test",
map[string]string{
"a": "apple",
"b": "banana",
},
)

hooks := map[string]*HookConfig{
"bash": {
Expand Down Expand Up @@ -286,7 +293,7 @@ func Test_GetScript_Validation(t *testing.T) {
err := os.WriteFile("my-script.ps1", nil, osutil.PermissionFile)
require.NoError(t, err)

env := []string{}
env := environment.Ephemeral()

mockContext := mocks.NewMockContext(context.Background())
hooksManager := NewHooksManager(tempDir)
Expand Down
17 changes: 14 additions & 3 deletions cli/azd/pkg/project/service_target_aks.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ func (t *aksTarget) Deploy(
return ServiceDeploymentResult{}, err
}

if len(endpoints) > 0 {
// The AKS endpoints contain some additional identifying information
// Split on common to pull out the URL as the first segment
// The last endpoint in the array will be the most publicly exposed
endpointParts := strings.Split(endpoints[len(endpoints)-1], ",")
t.env.SetServiceProperty(t.config.Name, "ENDPOINT_URL", endpointParts[0])
if err := t.env.Save(); err != nil {
return ServiceDeploymentResult{}, fmt.Errorf("failed updating environment with endpoint url, %w", err)
}
}

return ServiceDeploymentResult{
TargetResourceId: azure.KubernetesServiceRID(
t.env.GetSubscriptionId(),
Expand Down Expand Up @@ -455,11 +466,11 @@ func (t *aksTarget) getServiceEndpoints(ctx context.Context, namespace string, s
var endpoints []string
if service.Spec.Type == kubectl.ServiceTypeLoadBalancer {
for _, resource := range service.Status.LoadBalancer.Ingress {
endpoints = append(endpoints, fmt.Sprintf("http://%s (Service, Type: LoadBalancer)", resource.Ip))
endpoints = append(endpoints, fmt.Sprintf("http://%s, (Service, Type: LoadBalancer)", resource.Ip))
}
} else if service.Spec.Type == kubectl.ServiceTypeClusterIp {
for index, ip := range service.Spec.ClusterIps {
endpoints = append(endpoints, fmt.Sprintf("http://%s:%d (Service, Type: ClusterIP)", ip, service.Spec.Ports[index].Port))
endpoints = append(endpoints, fmt.Sprintf("http://%s:%d, (Service, Type: ClusterIP)", ip, service.Spec.Ports[index].Port))
}
}

Expand Down Expand Up @@ -495,7 +506,7 @@ func (t *aksTarget) getIngressEndpoints(ctx context.Context, namespace string, r
return nil, fmt.Errorf("failed constructing service endpoints, %w", err)
}

endpoints = append(endpoints, fmt.Sprintf("%s (Ingress, Type: LoadBalancer)", endpointUrl))
endpoints = append(endpoints, fmt.Sprintf("%s, (Ingress, Type: LoadBalancer)", endpointUrl))
}

return endpoints, nil
Expand Down