From 1aaa68bd43f647a297c919b5a089bbe3754e66fd Mon Sep 17 00:00:00 2001 From: magodo Date: Fri, 9 Aug 2024 16:11:52 +0800 Subject: [PATCH] `aztfexport res` supports multiple resources (#552) --- command_before_func.go | 17 ++- command_before_func_test.go | 2 +- flag.go | 22 +-- internal/meta/meta_res.go | 130 ++++++++++++------ internal/test/cases/case_vnet.go | 85 ++++++++++++ internal/test/resmap/e2e_cases_test.go | 7 + internal/test/resource/e2e_cases_test.go | 70 +++++++--- internal/test/resourcegroup/e2e_cases_test.go | 7 + main.go | 94 ++++++++----- pkg/config/config.go | 6 +- pkg/meta/meta.go | 2 +- 11 files changed, 333 insertions(+), 109 deletions(-) create mode 100644 internal/test/cases/case_vnet.go diff --git a/command_before_func.go b/command_before_func.go index 59d1a23..dc28ea6 100644 --- a/command_before_func.go +++ b/command_before_func.go @@ -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 { @@ -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 { diff --git a/command_before_func_test.go b/command_before_func_test.go index cf7a689..ba2b1f1 100644 --- a/command_before_func_test.go +++ b/command_before_func_test.go @@ -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 { diff --git a/flag.go b/flag.go index 2967354..0a79ffb 100644 --- a/flag.go +++ b/flag.go @@ -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 @@ -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 @@ -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) diff --git a/internal/meta/meta_res.go b/internal/meta/meta_res.go index 83cb539..62853bc 100644 --- a/internal/meta/meta_res.go +++ b/internal/meta/meta_res.go @@ -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) { @@ -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) diff --git a/internal/test/cases/case_vnet.go b/internal/test/cases/case_vnet.go new file mode 100644 index 0000000..69233e3 --- /dev/null +++ b/internal/test/cases/case_vnet.go @@ -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 +} diff --git a/internal/test/resmap/e2e_cases_test.go b/internal/test/resmap/e2e_cases_test.go index 8f2d8e3..27cb8cb 100644 --- a/internal/test/resmap/e2e_cases_test.go +++ b/internal/test/resmap/e2e_cases_test.go @@ -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) diff --git a/internal/test/resource/e2e_cases_test.go b/internal/test/resource/e2e_cases_test.go index 2a5d9fe..4da2859 100644 --- a/internal/test/resource/e2e_cases_test.go +++ b/internal/test/resource/e2e_cases_test.go @@ -69,26 +69,29 @@ func runCase(t *testing.T, d test.Data, c cases.Case) { cred, clientOpt := test.BuildCredAndClientOpt(t) - for idx, rctx := range l { - cfg := internalconfig.NonInteractiveModeConfig{ - Config: config.Config{ - CommonConfig: config.CommonConfig{ - Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), - SubscriptionId: os.Getenv("ARM_SUBSCRIPTION_ID"), - AzureSDKCredential: cred, - AzureSDKClientOption: *clientOpt, - OutputDir: aztfexportDir, - BackendType: "local", - DevProvider: true, - Parallelism: 1, - ProviderName: "azurerm", - }, - ResourceId: rctx.AzureId, - TFResourceName: fmt.Sprintf("res-%d", idx), + cfg := internalconfig.NonInteractiveModeConfig{ + Config: config.Config{ + CommonConfig: config.CommonConfig{ + Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + SubscriptionId: os.Getenv("ARM_SUBSCRIPTION_ID"), + AzureSDKCredential: cred, + AzureSDKClientOption: *clientOpt, + OutputDir: aztfexportDir, + BackendType: "local", + DevProvider: true, + Parallelism: 1, + ProviderName: "azurerm", }, - PlainUI: true, - } - t.Logf("Resource importing %s\n", rctx.AzureId) + }, + PlainUI: true, + } + + // Test single resouce mode + for idx, rctx := range l { + cfg.ResourceIds = []string{rctx.AzureId} + cfg.TFResourceName = fmt.Sprintf("res-%d", idx) + + t.Logf("Single resource importing %s\n", rctx.AzureId) if err := utils.RemoveEverythingUnder(cfg.OutputDir); err != nil { t.Fatalf("failed to clean up the output directory: %v", err) } @@ -97,7 +100,36 @@ func runCase(t *testing.T, d test.Data, c cases.Case) { } test.Verify(t, ctx, aztfexportDir, tfexecPath, rctx.ExpectResourceCount) } + + // Test multi-resource mode + var resourceIds []string + var total int + for _, rctx := range l { + resourceIds = append(resourceIds, rctx.AzureId) + total += rctx.ExpectResourceCount + } + + cfg.ResourceNamePattern = "res-" + cfg.ResourceIds = resourceIds + cfg.CommonConfig.Parallelism = total + + t.Logf("Multiple resources importing %v\n", resourceIds) + if err := utils.RemoveEverythingUnder(cfg.OutputDir); err != nil { + t.Fatalf("failed to clean up the output directory: %v", err) + } + if err := internal.BatchImport(ctx, cfg); err != nil { + t.Fatalf("failed to run resource import: %v", err) + } + test.Verify(t, ctx, aztfexportDir, tfexecPath, total) } + +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) diff --git a/internal/test/resourcegroup/e2e_cases_test.go b/internal/test/resourcegroup/e2e_cases_test.go index 6b25127..6514e70 100644 --- a/internal/test/resourcegroup/e2e_cases_test.go +++ b/internal/test/resourcegroup/e2e_cases_test.go @@ -87,6 +87,13 @@ func runCase(t *testing.T, d test.Data, c cases.Case) { test.Verify(t, ctx, aztfexportDir, tfexecPath, c.Total()) } +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) diff --git a/main.go b/main.go index bb3167d..91ba8de 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "context" "encoding/json" @@ -370,16 +371,23 @@ func main() { &cli.StringFlag{ Name: "name", EnvVars: []string{"AZTFEXPORT_NAME"}, - Usage: `The Terraform resource name.`, - Value: "res-0", + Usage: `The Terraform resource name (only works for single resource mode).`, Destination: &flagset.flagResName, }, &cli.StringFlag{ Name: "type", EnvVars: []string{"AZTFEXPORT_TYPE"}, - Usage: `The Terraform resource type.`, + Usage: `The Terraform resource type (only works for single resource mode).`, Destination: &flagset.flagResType, }, + &cli.StringFlag{ + Name: "name-pattern", + EnvVars: []string{"AZTFEXPORT_NAME_PATTERN"}, + Aliases: []string{"p"}, + Usage: `The pattern of the resource name. The semantic of a pattern is the same as Go's os.CreateTemp() (only works for multi-resource mode).`, + Value: "res-", + Destination: &flagset.flagPattern, + }, }, commonFlags...) resourceGroupFlags := append([]cli.Flag{ @@ -426,12 +434,12 @@ func main() { Commands: []*cli.Command{ { Name: "config", - Usage: `Configuring the tool`, + Usage: `Configuring the tool.`, UsageText: "aztfexport config [subcommand]", Subcommands: []*cli.Command{ { Name: "set", - Usage: `Set a configuration item for aztfexport`, + Usage: `Set a configuration item for aztfexport.`, UsageText: "aztfexport config set key value", Action: func(c *cli.Context) error { if c.NArg() != 2 { @@ -446,7 +454,7 @@ func main() { }, { Name: "get", - Usage: `Get a configuration item for aztfexport`, + Usage: `Get a configuration item for aztfexport.`, UsageText: "aztfexport config get key", Action: func(c *cli.Context) error { if c.NArg() != 1 { @@ -464,7 +472,7 @@ func main() { }, { Name: "show", - Usage: `Show the full configuration for aztfexport`, + Usage: `Show the full configuration for aztfexport.`, UsageText: "aztfexport config show", Action: func(c *cli.Context) error { cfg, err := cfgfile.GetConfig() @@ -482,24 +490,47 @@ func main() { }, }, { - Name: ModeResource, + Name: string(ModeResource), Aliases: []string{"res"}, - Usage: "Exporting a single resource", - UsageText: "aztfexport resource [option] ", + Usage: "Exporting one or more resources. The arguments can be resource ids, or path to files (prefixed with `@`) that contain resource id in each line.", + UsageText: "aztfexport resource [option] [ | @...]", Flags: resourceFlags, - Before: commandBeforeFunc(&flagset), + Before: commandBeforeFunc(&flagset, ModeResource), Action: func(c *cli.Context) error { if c.NArg() == 0 { return fmt.Errorf("No resource id specified") } - if c.NArg() > 1 { - return fmt.Errorf("More than one resource ids specified") - } - resId := c.Args().First() - - if _, err := armid.ParseResourceId(resId); err != nil { - return fmt.Errorf("invalid resource id: %v", err) + var resIds []string + for _, arg := range c.Args().Slice() { + if !strings.HasPrefix(arg, "@") { + if _, err := armid.ParseResourceId(arg); err != nil { + return fmt.Errorf("invalid resource id: %v", err) + } + resIds = append(resIds, arg) + continue + } + + path := strings.TrimPrefix(arg, "@") + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open file %q: %v", path, err) + } + + if err := func() error { + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + resId := strings.TrimSpace(scanner.Text()) + if _, err := armid.ParseResourceId(resId); err != nil { + return fmt.Errorf("invalid resource id (contained in %q): %v", path, err) + } + resIds = append(resIds, resId) + } + return scanner.Err() + }(); err != nil { + return fmt.Errorf("scanning %q: %v", path, err) + } } commonConfig, err := flagset.BuildCommonConfig() @@ -509,22 +540,23 @@ func main() { // Initialize the config cfg := config.Config{ - CommonConfig: commonConfig, - ResourceId: resId, - TFResourceName: flagset.flagResName, - TFResourceType: flagset.flagResType, + CommonConfig: commonConfig, + ResourceIds: resIds, + TFResourceName: flagset.flagResName, + TFResourceType: flagset.flagResType, + ResourceNamePattern: flagset.flagPattern, } return realMain(c.Context, cfg, flagset.flagNonInteractive, flagset.hflagMockClient, flagset.flagPlainUI, flagset.flagGenerateMappingFile, flagset.hflagProfile, flagset.DescribeCLI(ModeResource), flagset.hflagTFClientPluginPath) }, }, { - Name: ModeResourceGroup, + Name: string(ModeResourceGroup), Aliases: []string{"rg"}, - Usage: "Exporting a resource group and the nested resources resides within it", + Usage: "Exporting a resource group and the nested resources resides within it.", UsageText: "aztfexport resource-group [option] ", Flags: resourceGroupFlags, - Before: commandBeforeFunc(&flagset), + Before: commandBeforeFunc(&flagset, ModeResourceGroup), Action: func(c *cli.Context) error { if c.NArg() == 0 { return fmt.Errorf("No resource group specified") @@ -553,11 +585,11 @@ func main() { }, }, { - Name: ModeQuery, - Usage: "Exporting a customized scope of resources determined by an Azure Resource Graph where predicate", + Name: string(ModeQuery), + Usage: "Exporting a customized scope of resources determined by an Azure Resource Graph where predicate.", UsageText: "aztfexport query [option] ", Flags: queryFlags, - Before: commandBeforeFunc(&flagset), + Before: commandBeforeFunc(&flagset, ModeQuery), Action: func(c *cli.Context) error { if c.NArg() == 0 { return fmt.Errorf("No query specified") @@ -587,12 +619,12 @@ func main() { }, }, { - Name: ModeMappingFile, + Name: string(ModeMappingFile), Aliases: []string{"map"}, - Usage: "Exporting a customized scope of resources determined by the resource mapping file", + Usage: "Exporting a customized scope of resources determined by the resource mapping file.", UsageText: "aztfexport mapping-file [option] ", Flags: mappingFileFlags, - Before: commandBeforeFunc(&flagset), + Before: commandBeforeFunc(&flagset, ModeMappingFile), Action: func(c *cli.Context) error { if c.NArg() == 0 { return fmt.Errorf("No resource mapping file specified") diff --git a/pkg/config/config.go b/pkg/config/config.go index 2004886..49c6023 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -103,8 +103,8 @@ type Config struct { // Exactly one of below is specified - // ResourceId specifies the Azure resource id, this indicates the resource mode. - ResourceId string + // ResourceIds specifies the list of Azure resource ids, this indicates the resource mode. + ResourceIds []string // ResourceGroupName specifies the name of the resource group, this indicates the resource group mode. ResourceGroupName string // ARGPredicate specifies the ARG where predicate, this indicates the query mode. @@ -112,7 +112,7 @@ type Config struct { // MappingFile specifies the path of mapping file, this indicates the map file mode. MappingFile string - // ResourceNamePattern specifies the resource name pattern, this only applies to resource group mode and query mode. + // ResourceNamePattern specifies the resource name pattern, this only applies to resource group mode, query mode and multi-resource mode. ResourceNamePattern string // RecursiveQuery specifies whether to recursively list the child/proxy resources of the ARG resulted resource list, this only applies to query mode. diff --git a/pkg/meta/meta.go b/pkg/meta/meta.go index a50f01a..196b5c1 100644 --- a/pkg/meta/meta.go +++ b/pkg/meta/meta.go @@ -27,7 +27,7 @@ func NewMeta(cfg config.Config) (Meta, error) { return meta.NewMetaQuery(cfg) case cfg.MappingFile != "": return meta.NewMetaMap(cfg) - case cfg.ResourceId != "": + case len(cfg.ResourceIds) != 0: return meta.NewMetaResource(cfg) default: return nil, fmt.Errorf("invalid group config")