Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

emulate az with azd - for terraform only #3971

Closed
wants to merge 14 commits into from
13 changes: 13 additions & 0 deletions cli/azd/cmd/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"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/output"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -47,6 +48,9 @@ func (f *authTokenFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComma
f.global = global
local.StringArrayVar(&f.scopes, "scope", nil, "The scope to use when requesting an access token")
local.StringVar(&f.tenantID, "tenant-id", "", "The tenant id to use when requesting an access token.")
if exec.IsAzEmulator() {
vhvb1989 marked this conversation as resolved.
Show resolved Hide resolved
local.StringVar(&f.tenantID, "tenant", "", "The tenant id to use when requesting an access token.")
}
}

type CredentialProviderFn func(context.Context, *auth.CredentialForCurrentUserOptions) (azcore.TokenCredential, error)
Expand Down Expand Up @@ -169,6 +173,15 @@ func (a *authTokenAction) Run(ctx context.Context) (*actions.ActionResult, error
return nil, fmt.Errorf("fetching token: %w", err)
}

if exec.IsAzEmulator() {
res := contracts.AzEmulateAuthTokenResult{
AccessToken: token.Token,
ExpiresOn: contracts.RFC3339Time(token.ExpiresOn),
}

return nil, a.formatter.Format(res, a.writer, nil)
}

res := contracts.AuthTokenResult{
Token: token.Token,
ExpiresOn: contracts.RFC3339Time(token.ExpiresOn),
Expand Down
112 changes: 112 additions & 0 deletions cli/azd/cmd/az_cli_emulate_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package cmd

import (
"context"
"encoding/json"
"fmt"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func azCliEmulateAccountCommands(root *actions.ActionDescriptor) *actions.ActionDescriptor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned it today during our meeting, but it'd be nice to have a true emulation where we shim the few commands we need, and the rest are passthrough. I think this would maybe require thinking about changing how commands are registered in emulator mode.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the passthrough-proxy is a little complicated due to authentication and current state of az.
Before investing time on building that, I would rather wait to see if we ever need to do passthrough for any tool. Right now, the passthrough is not required for the current scenario.

Also, the az emulator is just a temporary solution; to be removed by oneAuth eventually. So, I don't think it should have lot of investment.

group := root.Add("account", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Use: "account",
Short: "Emulates az account commands",
Hidden: true,
},
})

group.Add("show", &actions.ActionDescriptorOptions{
Command: newAccountShowCmd(),
FlagsResolver: newAccountShowFlags,
ActionResolver: newAccountAction,
OutputFormats: []output.Format{output.JsonFormat},
DefaultFormat: output.JsonFormat,
})

group.Add("get-access-token", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Use: "get-access-token",
Hidden: true,
},
FlagsResolver: newAuthTokenFlags,
ActionResolver: newAuthTokenAction,
OutputFormats: []output.Format{output.JsonFormat},
DefaultFormat: output.JsonFormat,
})

return group
}

type accountShowFlags struct {
global *internal.GlobalCommandOptions
internal.EnvFlag
}

func (s *accountShowFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
s.EnvFlag.Bind(local, global)
s.global = global
}

func newAccountShowFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *accountShowFlags {
flags := &accountShowFlags{}
flags.Bind(cmd.Flags(), global)

return flags
}

func newAccountShowCmd() *cobra.Command {
return &cobra.Command{
Use: "show",
Hidden: true,
}
}

type accountShowAction struct {
env *environment.Environment
console input.Console
subManager *account.SubscriptionsManager
}

func newAccountAction(
console input.Console,
env *environment.Environment,
subManager *account.SubscriptionsManager,
) actions.Action {
return &accountShowAction{
console: console,
env: env,
subManager: subManager,
}
}

type accountShowOutput struct {
Id string `json:"id"`
TenantId string `json:"tenantId"`
}

func (s *accountShowAction) Run(ctx context.Context) (*actions.ActionResult, error) {
subId := s.env.GetSubscriptionId()
tenantId, err := s.subManager.LookupTenant(ctx, subId)
if err != nil {
return nil, err
}
o := accountShowOutput{
Id: subId,
TenantId: tenantId,
}
output, err := json.Marshal(o)
if err != nil {
return nil, err
}
fmt.Fprint(s.console.Handles().Stdout, string(output))
return nil, nil
}
1 change: 1 addition & 0 deletions cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func NewRootCmd(
templatesActions(root)
authActions(root)
hooksActions(root)
azCliEmulateAccountCommands(root)

root.Add("version", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Expand Down
14 changes: 14 additions & 0 deletions cli/azd/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"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"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -54,6 +55,19 @@ func newVersionAction(
}

func (v *versionAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// fake-az in env makes azd to simulate az cli output.
// This is to make tools like terraform to use azd when they thing they are using az.
if exec.IsAzEmulator() {
fmt.Fprintf(v.console.Handles().Stdout, `{
"azure-cli": "2.61.0",
"azure-cli-core": "2.61.0",
"azure-cli-telemetry": "1.1.0",
"extensions": {}
}
`)
return nil, nil
}

switch v.formatter.Kind() {
case output.NoneFormat:
fmt.Fprintf(v.console.Handles().Stdout, "azd version %s\n", internal.Version)
Expand Down
10 changes: 10 additions & 0 deletions cli/azd/pkg/contracts/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"time"

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

// AuthTokenResult is the value returned by `azd get-access-token`. It matches the shape of `azcore.AccessToken`
Expand All @@ -17,11 +19,19 @@ type AuthTokenResult struct {
ExpiresOn RFC3339Time `json:"expiresOn"`
}

type AzEmulateAuthTokenResult struct {
AccessToken string `json:"accessToken"`
ExpiresOn RFC3339Time `json:"expiresOn"`
}

// RFC3339Time is a time.Time that uses time.RFC3339 format when marshaling to JSON, not time.RFC3339Nano as
// the standard library time.Time does.
type RFC3339Time time.Time

func (r RFC3339Time) MarshalJSON() ([]byte, error) {
if exec.IsAzEmulator() {
return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format("2006-01-02 15:04:05.000000"))), nil
}
return []byte(fmt.Sprintf(`"%s"`, time.Time(r).Format(time.RFC3339))), nil
}

Expand Down
61 changes: 61 additions & 0 deletions cli/azd/pkg/exec/az_emulator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package exec

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

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

const (
emulatorEnvName string = "AZURE_AZ_EMULATOR"
)

// IsAzEmulator returns true if the AZURE_AZ_EMULATOR environment variable is defined.
// It does not matter the value of the environment variable, as long as it is defined.
func IsAzEmulator() bool {
_, emulateEnvVarDefined := os.LookupEnv(emulatorEnvName)
return emulateEnvVarDefined
}

// creates a copy of azd binary and renames it to az and returns the path to it
func emulateAzFromPath() (string, error) {
path, err := exec.LookPath("azd")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using os.Executable()? The azd in PATH is not necessarily the one being executed. The process executing may not be in PATH at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea. Not sure if it helps or not when running with debug.
Limiting to using PATH is at least a way to require azd to be installed and fail on non-standard cases. azd installation scripts will all set azd in the PATH

if err != nil {
return "", fmt.Errorf("azd binary not found in PATH: %w", err)
}
azdConfigPath, err := config.GetUserConfigDir()
if err != nil {
return "", fmt.Errorf("could not get user config dir: %w", err)
}
emuPath := filepath.Join(azdConfigPath, "bin", "azEmulate")
err = os.MkdirAll(emuPath, osutil.PermissionDirectoryOwnerOnly)
if err != nil {
return "", fmt.Errorf("could not create directory for azEmulate: %w", err)
}
emuPath = filepath.Join(emuPath, strings.ReplaceAll(filepath.Base(path), "azd", "az"))

srcFile, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("opening src: %w", err)
}
defer srcFile.Close()

destFile, err := os.OpenFile(emuPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return "", fmt.Errorf("creating dest: %w", err)
}
defer destFile.Close()

_, err = io.Copy(destFile, srcFile)
vhvb1989 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", fmt.Errorf("copying binary: %w", err)
}

return emuPath, nil
}
Loading
Loading