Skip to content

Commit

Permalink
aztfexport res supports multiple resources (#552)
Browse files Browse the repository at this point in the history
  • Loading branch information
magodo authored Aug 9, 2024
1 parent 6886573 commit 1aaa68b
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 109 deletions.
17 changes: 15 additions & 2 deletions command_before_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/urfave/cli/v2"
)

func commandBeforeFunc(fset *FlagSet) func(ctx *cli.Context) error {
return func(_ *cli.Context) error {
func commandBeforeFunc(fset *FlagSet, mode Mode) func(ctx *cli.Context) error {
return func(ctx *cli.Context) error {
// Common flags check
if fset.flagAppend {
if fset.flagOverwrite {
Expand Down Expand Up @@ -103,6 +103,19 @@ func commandBeforeFunc(fset *FlagSet) func(ctx *cli.Context) error {
return err
}

// Mode specific flags check
switch mode {
case ModeResource:
if ctx.Args().Len() > 1 || (ctx.Args().Len() == 1 && strings.HasPrefix(ctx.Args().First(), "@")) {
if fset.flagResType != "" {
return fmt.Errorf("`--type` can't be specified for multi-resource mode")
}
if fset.flagResName != "" {
return fmt.Errorf("`--name` can't be specified for multi-resource mode")
}
}
}

// Initialize output directory
if _, err := os.Stat(fset.flagOutputDir); os.IsNotExist(err) {
if err := os.MkdirAll(fset.flagOutputDir, 0750); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion command_before_func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func TestCommondBeforeFunc(t *testing.T) {
tt.fset.flagSubscriptionId = "test"
}

err := commandBeforeFunc(&tt.fset)(nil)
err := commandBeforeFunc(&tt.fset, "")(nil)
if tt.err == "" {
require.NoError(t, err)
if tt.postCheck != nil {
Expand Down
22 changes: 14 additions & 8 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ type FlagSet struct {
// Subcommand specific flags
//
// res:
// flagResName
// flagResType
// flagResName (for single resource)
// flagResType (for single resource)
// flagPattern (for multi resources)
//
// rg:
// flagPattern
Expand All @@ -94,18 +95,20 @@ type FlagSet struct {
flagIncludeResourceGroup bool
}

type Mode string

const (
ModeResource = "resource"
ModeResourceGroup = "resource-group"
ModeQuery = "query"
ModeMappingFile = "mapping-file"
ModeResource Mode = "resource"
ModeResourceGroup Mode = "resource-group"
ModeQuery Mode = "query"
ModeMappingFile Mode = "mapping-file"
)

// DescribeCLI construct a description of the CLI based on the flag set and the specified mode.
// The main reason is to record the usage of some "interesting" options in the telemetry.
// Note that only insensitive values are recorded (i.e. subscription id, resource id, etc are not recorded)
func (flag FlagSet) DescribeCLI(mode string) string {
args := []string{mode}
func (flag FlagSet) DescribeCLI(mode Mode) string {
args := []string{string(mode)}

// The following flags are skipped eiter not interesting, or might contain sensitive info:
// - flagOutputDir
Expand Down Expand Up @@ -225,6 +228,9 @@ func (flag FlagSet) DescribeCLI(mode string) string {
if flag.flagResType != "" {
args = append(args, "--type="+flag.flagResType)
}
if flag.flagPattern != "" {
args = append(args, "--name-pattern="+flag.flagPattern)
}
case ModeResourceGroup:
if flag.flagPattern != "" {
args = append(args, "--name-pattern="+flag.flagPattern)
Expand Down
130 changes: 86 additions & 44 deletions internal/meta/meta_res.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (

type MetaResource struct {
baseMeta
AzureId armid.ResourceId
ResourceName string
ResourceType string
AzureIds []armid.ResourceId
ResourceName string
ResourceType string
resourceNamePrefix string
resourceNameSuffix string
}

func NewMetaResource(cfg config.Config) (*MetaResource, error) {
Expand All @@ -25,79 +27,119 @@ func NewMetaResource(cfg config.Config) (*MetaResource, error) {
return nil, err
}

id, err := armid.ParseResourceId(cfg.ResourceId)
if err != nil {
return nil, err
var ids []armid.ResourceId

for _, id := range cfg.ResourceIds {
id, err := armid.ParseResourceId(id)
if err != nil {
return nil, err
}
ids = append(ids, id)
}

meta := &MetaResource{
baseMeta: *baseMeta,
AzureId: id,
AzureIds: ids,
ResourceName: cfg.TFResourceName,
ResourceType: cfg.TFResourceType,
}

meta.resourceNamePrefix, meta.resourceNameSuffix = resourceNamePattern(cfg.ResourceNamePattern)

return meta, nil
}

func (meta MetaResource) ScopeName() string {
return meta.AzureId.String()
if len(meta.AzureIds) == 1 {
return meta.AzureIds[0].String()
} else {
return meta.AzureIds[0].String() + " and more..."
}
}

func (meta *MetaResource) ListResource(_ context.Context) (ImportList, error) {
resourceSet := &resourceset.AzureResourceSet{
Resources: []resourceset.AzureResource{
{
Id: meta.AzureId,
},
},
func (meta *MetaResource) ListResource(ctx context.Context) (ImportList, error) {
var resources []resourceset.AzureResource
for _, id := range meta.AzureIds {
resources = append(resources, resourceset.AzureResource{Id: id})
}

rset := &resourceset.AzureResourceSet{
Resources: resources,
}

meta.Logger().Debug("Azure Resource set map to TF resource set")

var rl []resourceset.TFResource
if meta.useAzAPI() {
rl = resourceSet.ToTFAzAPIResources()
rl = rset.ToTFAzAPIResources()
} else {
rl = resourceSet.ToTFAzureRMResources(meta.Logger(), meta.parallelism, meta.azureSDKCred, meta.azureSDKClientOpt)
rl = rset.ToTFAzureRMResources(meta.Logger(), meta.parallelism, meta.azureSDKCred, meta.azureSDKClientOpt)
}

// This is to record known resource types. In case there is a known resource type and there comes another same typed resource,
// then we need to modify the resource name. Otherwise, there will be a resource address conflict.
// See https://github.com/Azure/aztfexport/issues/275 for an example.
rtCnt := map[string]int{}

var l ImportList
for _, res := range rl {

// The ResourceName and ResourceType are only honored for single resource
if len(rl) == 1 {
res := rl[0]

// Honor the ResourceName
name := meta.ResourceName
rtCnt[res.TFType]++
if rtCnt[res.TFType] > 1 {
name += fmt.Sprintf("-%d", rtCnt[res.TFType]-1)
if name == "" {
name = fmt.Sprintf("%s%d%s", meta.resourceNamePrefix, 0, meta.resourceNameSuffix)
}

// Honor the ResourceType
tftype := res.TFType
tfid := res.TFId
if meta.ResourceType != "" && meta.ResourceType != res.TFType {
// res.TFType can be either empty (if aztft failed to query), or not.
// If the user has specified a different type, then use it.
tftype = meta.ResourceType

// Also use this resource type to requery its resource id.
var err error
tfid, err = aztft.QueryId(res.AzureId.String(), meta.ResourceType,
&aztft.APIOption{
Cred: meta.azureSDKCred,
ClientOption: meta.azureSDKClientOpt,
})
if err != nil {
return nil, err
}
}

tfAddr := tfaddr.TFAddr{
Type: res.TFType, // this might be empty if have multiple matches in aztft
Type: tftype,
Name: name,
}

item := ImportItem{
AzureResourceID: res.AzureId,
TFResourceId: res.TFId, // this might be empty if have multiple matches in aztft
TFResourceId: tfid,
TFAddr: tfAddr,
TFAddrCache: tfAddr,
}
l = append(l, item)
return l, nil
}

// Some special Azure resource is missing the essential property that is used by aztft to detect their TF resource type.
// In this case, users can use the `--type` option to manually specify the TF resource type.
if meta.ResourceType != "" && !meta.useAzAPI() {
if meta.AzureId.Equal(res.AzureId) {
tfid, err := aztft.QueryId(meta.AzureId.String(), meta.ResourceType,
&aztft.APIOption{
Cred: meta.azureSDKCred,
ClientOption: meta.azureSDKClientOpt,
})
if err != nil {
return nil, err
}
item.TFResourceId = tfid
item.TFAddr.Type = meta.ResourceType
item.TFAddrCache.Type = meta.ResourceType
}
// Multi-resource mode only honors the resourceName[Pre|Suf]fix
for i, res := range rl {
tfAddr := tfaddr.TFAddr{
Type: "",
Name: fmt.Sprintf("%s%d%s", meta.resourceNamePrefix, i, meta.resourceNameSuffix),
}
item := ImportItem{
AzureResourceID: res.AzureId,
TFResourceId: res.TFId,
TFAddr: tfAddr,
TFAddrCache: tfAddr,
}
if res.TFType != "" {
item.TFAddr.Type = res.TFType
item.TFAddrCache.Type = res.TFType
item.Recommendations = []string{res.TFType}
item.IsRecommended = true
}

l = append(l, item)
Expand Down
85 changes: 85 additions & 0 deletions internal/test/cases/case_vnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cases

import (
"fmt"

"github.com/Azure/aztfexport/internal/resmap"
"github.com/Azure/aztfexport/internal/test"
)

var _ Case = CaseVnet{}

type CaseVnet struct{}

func (CaseVnet) Tpl(d test.Data) string {
return fmt.Sprintf(`
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = false
}
}
}
resource "azurerm_resource_group" "test" {
name = "%[1]s"
location = "WestEurope"
}
resource "azurerm_virtual_network" "test" {
name = "aztfexport-test-%[2]s"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
}
resource "azurerm_subnet" "test" {
name = "internal"
resource_group_name = azurerm_resource_group.test.name
virtual_network_name = azurerm_virtual_network.test.name
address_prefixes = ["10.0.2.0/24"]
}
`, d.RandomRgName(), d.RandomStringOfLength(8))
}

func (CaseVnet) Total() int {
return 3
}

func (CaseVnet) ResourceMapping(d test.Data) (resmap.ResourceMapping, error) {
return test.ResourceMapping(fmt.Sprintf(`{
{{ "/subscriptions/%[1]s/resourcegroups/%[2]s" | Quote }}: {
"resource_type": "azurerm_resource_group",
"resource_name": "test",
"resource_id": "/subscriptions/%[1]s/resourceGroups/%[2]s"
},
{{ "/subscriptions/%[1]s/resourcegroups/%[2]s/providers/microsoft.network/virtualnetworks/aztfexport-test-%[3]s" | Quote }}: {
"resource_type": "azurerm_virtual_network",
"resource_name": "test",
"resource_id": "/subscriptions/%[1]s/resourceGroups/%[2]s/providers/Microsoft.Network/virtualNetworks/aztfexport-test-%[3]s"
},
{{ "/subscriptions/%[1]s/resourcegroups/%[2]s/providers/microsoft.network/virtualnetworks/aztfexport-test-%[3]s/subnets/internal" | Quote }}: {
"resource_type": "azurerm_subnet",
"resource_name": "test",
"resource_id": "/subscriptions/%[1]s/resourceGroups/%[2]s/providers/Microsoft.Network/virtualNetworks/aztfexport-test-%[3]s/subnets/internal"
}
}
`, d.SubscriptionId, d.RandomRgName(), d.RandomStringOfLength(8)))
}

func (CaseVnet) SingleResourceContext(d test.Data) ([]SingleResourceContext, error) {
return []SingleResourceContext{
{
AzureId: fmt.Sprintf("/subscriptions/%[1]s/resourceGroups/%[2]s", d.SubscriptionId, d.RandomRgName()),
ExpectResourceCount: 1,
},
{
AzureId: fmt.Sprintf("/subscriptions/%[1]s/resourceGroups/%[2]s/providers/Microsoft.Network/virtualNetworks/aztfexport-test-%[3]s", d.SubscriptionId, d.RandomRgName(), d.RandomStringOfLength(8)),
ExpectResourceCount: 1,
},
{
AzureId: fmt.Sprintf("/subscriptions/%[1]s/resourceGroups/%[2]s/providers/Microsoft.Network/virtualNetworks/aztfexport-test-%[3]s/subnets/internal", d.SubscriptionId, d.RandomRgName(), d.RandomStringOfLength(8)),
ExpectResourceCount: 1,
},
}, nil
}
7 changes: 7 additions & 0 deletions internal/test/resmap/e2e_cases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ func runCase(t *testing.T, d test.Data, c cases.Case) {
test.Verify(t, ctx, aztfexportDir, tfexecPath, len(resMapping))
}

func TestVnet(t *testing.T) {
t.Parallel()
test.Precheck(t)
c, d := cases.CaseVnet{}, test.NewData()
runCase(t, d, c)
}

func TestComputeVMDisk(t *testing.T) {
t.Parallel()
test.Precheck(t)
Expand Down
Loading

0 comments on commit 1aaa68b

Please sign in to comment.