Skip to content

Commit

Permalink
Allow show to work when no environment exists (#2379)
Browse files Browse the repository at this point in the history
Today, `azd show` is used by VS Code to get information about the
current project, both the services that make it up (and information
about where the code for these services is) as well as the target
resources (if we can determine them)

In the case where an environment didn't exist (which is hard to have
happen during a normal course of action, but possible if you manually
go mucking around in `.azure`) `azd show` would prompt the user if
they wanted to go create an environment.

This caused issues with the VS integration, since the editor would not
expect `azd` to prompt in this case.

`azd show` already handles the case where the infrastructure for a
project has not been deployed. This change extends this behavior so
that if for some reason we can not fetch the current environment, we
just return information about the services that we can discover

Fixes #2296
Fixes #851
  • Loading branch information
ellismg committed Jun 12, 2023
1 parent a6ce781 commit 6c539fe
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 45 deletions.
106 changes: 61 additions & 45 deletions cli/azd/cmd/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,13 @@ func newShowCmd() *cobra.Command {
}

type showAction struct {
projectConfig *project.ProjectConfig
resourceManager project.ResourceManager
console input.Console
formatter output.Formatter
writer io.Writer
azCli azcli.AzCli
azdCtx *azdcontext.AzdContext
env *environment.Environment
flags *showFlags
projectConfig *project.ProjectConfig
console input.Console
formatter output.Formatter
writer io.Writer
azCli azcli.AzCli
azdCtx *azdcontext.AzdContext
flags *showFlags
}

func newShowAction(
Expand All @@ -68,21 +66,17 @@ func newShowAction(
writer io.Writer,
azCli azcli.AzCli,
projectConfig *project.ProjectConfig,
resourceManager project.ResourceManager,
azdCtx *azdcontext.AzdContext,
env *environment.Environment,
flags *showFlags,
) actions.Action {
return &showAction{
projectConfig: projectConfig,
resourceManager: resourceManager,
console: console,
formatter: formatter,
writer: writer,
azCli: azCli,
azdCtx: azdCtx,
env: env,
flags: flags,
projectConfig: projectConfig,
console: console,
formatter: formatter,
writer: writer,
azCli: azCli,
azdCtx: azdCtx,
flags: flags,
}
}

Expand Down Expand Up @@ -110,36 +104,58 @@ func (s *showAction) Run(ctx context.Context) (*actions.ActionResult, error) {

// Add information about the target of each service, if we can determine it (if the infrastructure has
// not been deployed, for example, we'll just not include target information)
if subId := s.env.GetSubscriptionId(); subId != "" {
resourceManager := infra.NewAzureResourceManager(s.azCli)
envName := s.env.GetEnvName()

rgName, err := resourceManager.FindResourceGroupForEnvironment(ctx, subId, envName)
if err == nil {
for svcName, serviceConfig := range s.projectConfig.Services {
if resources, err := s.resourceManager.GetServiceResources(ctx, subId, rgName, serviceConfig); err == nil {
resourceIds := make([]string, len(resources))
for idx, res := range resources {
resourceIds[idx] = res.Id
}
//
// Before we can discover resources, we need to load the current environment. We do this ourselves instead of
// having an environment injected into us so we can handle cases where the current environment doesn't exist (if we
// injected an environment, we'd prompt the user to see if they want to created one and we'd prefer not to have show
// interact with the user).
environmentName := s.flags.environmentName

if environmentName == "" {
var err error
environmentName, err = s.azdCtx.GetDefaultEnvironmentName()
if err != nil {
log.Printf("could not determine current environment: %s, resource ids will not be available", err)
}

resSvc := res.Services[svcName]
resSvc.Target = &contracts.ShowTargetArm{
ResourceIds: resourceIds,
}

if env, err := environment.GetEnvironment(s.azdCtx, environmentName); err != nil {
log.Printf("could not load environment: %s, resource ids will not be available", err)
} else {
if subId := env.GetSubscriptionId(); subId == "" {
log.Printf("provision has not been run, resource ids will not be available")
} else {
azureResourceManager := infra.NewAzureResourceManager(s.azCli)
resourceManager := project.NewResourceManager(env, s.azCli)
envName := env.GetEnvName()

rgName, err := azureResourceManager.FindResourceGroupForEnvironment(ctx, subId, envName)
if err == nil {
for svcName, serviceConfig := range s.projectConfig.Services {
resources, err := resourceManager.GetServiceResources(ctx, subId, rgName, serviceConfig)
if err == nil {
resourceIds := make([]string, len(resources))
for idx, res := range resources {
resourceIds[idx] = res.Id
}

resSvc := res.Services[svcName]
resSvc.Target = &contracts.ShowTargetArm{
ResourceIds: resourceIds,
}
res.Services[svcName] = resSvc
} else {
log.Printf("ignoring error determining resource id for service %s: %v", svcName, err)
}
res.Services[svcName] = resSvc
} else {
log.Printf("ignoring error determining resource id for service %s: %v", svcName, err)
}
} else {
log.Printf(
"ignoring error determining resource group for environment %s, resource ids will not be available: %v",
env.GetEnvName(),
err)
}
} else {
log.Printf(
"ignoring error determining resource group for environment %s, resource ids will not be available: %v",
s.env.GetEnvName(),
err)
}
} else {
log.Printf("provision has not been run, resource ids will not be available")
}

return nil, s.formatter.Format(res, s.writer, nil)
Expand Down
77 changes: 77 additions & 0 deletions cli/azd/test/functional/show_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cli_test

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/azure/azure-dev/cli/azd/test/azdcli"
"github.com/stretchr/testify/require"
)

func Test_CLI_ShowWorksWithoutEnvironment(t *testing.T) {
t.Parallel()
ctx, cancel := newTestContext(t)
defer cancel()

dir := tempDirWithDiagnostics(t)
t.Logf("DIR: %s", dir)

envName := randomEnvName()
t.Logf("AZURE_ENV_NAME: %s", envName)

cli := azdcli.NewCLI(t)
cli.WorkingDirectory = dir

err := copySample(dir, "webapp")
require.NoError(t, err, "failed expanding sample")

_, err = cli.RunCommandWithStdIn(ctx, stdinForInit(envName), "init")
require.NoError(t, err)

// Remove information about the just created enviroment to simulate the case where
// there's some issue with the environment and we can't load it.
err = os.RemoveAll(filepath.Join(dir, ".azure", envName))
require.NoError(t, err)

result, err := cli.RunCommand(ctx, "show", "--output", "json")
require.NoError(t, err)

var showRes struct {
Name string `json:"name"`
Services map[string]*struct {
Project struct {
Path string `json:"path"`
Language string `json:"language"`
} `json:"project"`
Target *struct {
ResourceIds []string `json:"resourceIds"`
} `json:"target"`
} `json:"services"`
}

err = json.Unmarshal([]byte(result.Stdout), &showRes)
require.NoError(t, err)

require.Equal(t, "webapp", showRes.Name)
require.Equal(t, 1, len(showRes.Services))
require.NotNil(t, showRes.Services["web"])
require.Nil(t, showRes.Services["web"].Target)

// Repeat the process but passing an explicit environment name for an environment that doesn't exist and ensure
// that we get the same result as above.
result, err = cli.RunCommand(ctx, "show", "-e", "does-not-exist-by-design", "--output", "json")
require.NoError(t, err)

err = json.Unmarshal([]byte(result.Stdout), &showRes)
require.NoError(t, err)

require.Equal(t, "webapp", showRes.Name)
require.Equal(t, 1, len(showRes.Services))
require.NotNil(t, showRes.Services["web"])
require.Nil(t, showRes.Services["web"].Target)
}

0 comments on commit 6c539fe

Please sign in to comment.