From 6dc981fda7659906c09851f80249975b8ff3b010 Mon Sep 17 00:00:00 2001 From: Heng Lu Date: Tue, 12 Sep 2023 15:02:59 +0800 Subject: [PATCH] support azapi dependencies --- CHANGELOG.md | 6 + autorest/config.go | 110 ++ autorest/config_test.go | 115 ++ autorest/testdata/readme.md | 620 ++++++ commands/auto.go | 84 - commands/cleanup.go | 49 +- commands/generate.go | 434 ++++- commands/generate_test.go | 12 +- commands/setup.go | 77 - commands/test.go | 14 +- commands/validate.go | 25 +- dependency/azapi.go | 89 + dependency/azapi_test.go | 39 + dependency/azurerm.go | 59 + dependency/azurerm/loader.go | 10 +- dependency/azurerm/loader_hardcode.go | 100 +- dependency/azurerm/loader_mapping_json.go | 24 +- dependency/azurerm_test.go | 28 + dependency/pattern.go | 62 + dependency/pattern_test.go | 147 ++ hcl/marshal.go | 19 +- main.go | 45 +- resource/context.go | 68 +- resource/context_test.go | 7 +- resource/from_example.go | 56 +- resource/from_example_test.go | 104 +- resource/from_swagger.go | 4 +- resource/from_swagger_test.go | 2 +- .../resolver/azapi_definition_resolver.go | 36 + .../resolver/azapi_dependency_resolver.go | 36 + .../resolver/azapi_resource_id_resolver.go | 38 + .../azapi_resource_placeholder_resolver.go | 44 + .../resolver/azurerm_dependency_resolver.go | 32 + .../resolver/existing_dependency_resolver.go | 108 ++ resource/resolver/known_reference_resolver.go | 27 + resource/resolver/location_id_resolver.go | 37 + resource/resolver/provider_id_resolver.go | 40 + resource/resolver/reference_resolver.go | 301 +-- .../createOrUpdateAutomationAccount.json | 63 + .../examples/deleteAutomationAccount.json | 12 + .../deserializeGraphRunbookContent.json | 28 + .../examples/getAutomationAccount.json | 32 + .../getStatisticsOfAutomationAccount.json | 108 ++ .../getUsagesOfAutomationAccount.json | 47 + .../examples/listAutomationAccountKeys.json | 27 + ...listAutomationAccountsByResourceGroup.json | 136 ++ .../listAutomationAccountsBySubscription.json | 685 +++++++ .../examples/listRestAPIOperations.json | 1673 +++++++++++++++++ .../createOrUpdateSourceControl.json | 62 + .../sourceControl/deleteSourceControl.json | 12 + .../sourceControl/getAllSourceControls.json | 91 + .../sourceControl/getSourceControl.json | 28 + .../updateSourceControl_patch.json | 42 + .../examples/updateAutomationAccount.json | 41 + resource/types/azapi_definition.go | 28 +- resource/types/azapi_definition_test.go | 61 +- swagger/swagger.go | 134 ++ swagger/swagger_test.go | 187 +- swagger/testdata/operations.json | 2 +- swagger/types.go | 20 + tf/terraform.go | 5 +- tf/utils.go | 18 +- utils/body.go | 41 + utils/body_test.go | 38 + utils/hcl.go | 20 + utils/hcl_test.go | 151 ++ utils/id.go | 118 ++ utils/id_test.go | 163 +- version.go | 2 +- 69 files changed, 6411 insertions(+), 772 deletions(-) create mode 100644 autorest/config.go create mode 100644 autorest/config_test.go create mode 100644 autorest/testdata/readme.md delete mode 100644 commands/auto.go delete mode 100644 commands/setup.go create mode 100644 dependency/azapi.go create mode 100644 dependency/azapi_test.go create mode 100644 dependency/azurerm.go create mode 100644 dependency/azurerm_test.go create mode 100644 dependency/pattern.go create mode 100644 dependency/pattern_test.go create mode 100644 resource/testdata/examples/createOrUpdateAutomationAccount.json create mode 100644 resource/testdata/examples/deleteAutomationAccount.json create mode 100644 resource/testdata/examples/deserializeGraphRunbookContent.json create mode 100644 resource/testdata/examples/getAutomationAccount.json create mode 100644 resource/testdata/examples/getStatisticsOfAutomationAccount.json create mode 100644 resource/testdata/examples/getUsagesOfAutomationAccount.json create mode 100644 resource/testdata/examples/listAutomationAccountKeys.json create mode 100644 resource/testdata/examples/listAutomationAccountsByResourceGroup.json create mode 100644 resource/testdata/examples/listAutomationAccountsBySubscription.json create mode 100644 resource/testdata/examples/listRestAPIOperations.json create mode 100644 resource/testdata/examples/sourceControl/createOrUpdateSourceControl.json create mode 100644 resource/testdata/examples/sourceControl/deleteSourceControl.json create mode 100644 resource/testdata/examples/sourceControl/getAllSourceControls.json create mode 100644 resource/testdata/examples/sourceControl/getSourceControl.json create mode 100644 resource/testdata/examples/sourceControl/updateSourceControl_patch.json create mode 100644 resource/testdata/examples/updateAutomationAccount.json create mode 100644 utils/body_test.go create mode 100644 utils/hcl_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 54a980d9..b815494b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## V0.12.0 +FEATURES: +- Generate multiple test cases from one or multiple swagger spec files. +- Support using verified azapi examples as automatically generated dependencies. +- Support `azapi_resource_id` data source as automatically generated dependencies. + ## v0.11.0 FEATURES: - Support coverage report diff --git a/autorest/config.go b/autorest/config.go new file mode 100644 index 00000000..b327c96f --- /dev/null +++ b/autorest/config.go @@ -0,0 +1,110 @@ +package autorest + +import ( + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/ast" + "github.com/gomarkdown/markdown/parser" + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +type Package struct { + Tag string + InputFiles []string +} + +type YamlPackage struct { + InputFiles []string `yaml:"input-file"` +} + +var r = regexp.MustCompile(`\$\(tag\)\s+==\s+'(.+)'`) + +func ParseAutoRestConfig(filename string) []Package { + data, err := os.ReadFile(filename) + if err != nil { + return nil + } + md := markdown.Parse(data, parser.NewWithExtensions(parser.NoExtensions)) + codeBlocks := allCodeBlocks(&md) + + out := make([]Package, 0) + for _, codeBlock := range codeBlocks { + if string(codeBlock.Info) == "yaml" { + yamlPackage, err := ParseYamlConfig(string(codeBlock.Literal)) + if err != nil { + logrus.Warnf("failed to parse yaml config: %+v", err) + } else { + for i, inputFile := range yamlPackage.InputFiles { + yamlPackage.InputFiles[i] = path.Clean(path.Join(path.Dir(filename), inputFile)) + } + out = append(out, *yamlPackage) + } + } + } + + return out +} + +func allCodeBlocks(node *ast.Node) []ast.CodeBlock { + if node == nil { + return nil + } + switch v := (*node).(type) { + case *ast.Container: + out := make([]ast.CodeBlock, 0) + for _, child := range v.Children { + out = append(out, allCodeBlocks(&child)...) + } + return out + case *ast.Document: + out := make([]ast.CodeBlock, 0) + for _, child := range v.Children { + out = append(out, allCodeBlocks(&child)...) + } + return out + case *ast.Paragraph: + out := make([]ast.CodeBlock, 0) + for _, child := range v.Children { + out = append(out, allCodeBlocks(&child)...) + } + return out + case *ast.CodeBlock: + return []ast.CodeBlock{*v} + } + return nil +} + +func ParseYamlConfig(content string) (*Package, error) { + matches := r.FindAllStringSubmatch(content, -1) + if len(matches) == 1 && len(matches[0]) == 2 { + tag := matches[0][1] + + index := strings.Index(content, "\n") + if index == -1 { + return nil, fmt.Errorf("invalid yaml code block: no newline after tag, input: %v", content) + } + + yamlContent := content[index+1:] + var yamlPackage YamlPackage + err := yaml.Unmarshal([]byte(yamlContent), &yamlPackage) + if err != nil { + return nil, err + } + + if len(yamlPackage.InputFiles) == 0 { + return nil, fmt.Errorf("input-file is empty, input: %v", content) + } + + return &Package{ + Tag: tag, + InputFiles: yamlPackage.InputFiles, + }, nil + } + return nil, fmt.Errorf("tag not found in yaml config: %s", content) +} diff --git a/autorest/config_test.go b/autorest/config_test.go new file mode 100644 index 00000000..e1890d42 --- /dev/null +++ b/autorest/config_test.go @@ -0,0 +1,115 @@ +package autorest + +import ( + "log" + "os" + "path" + "testing" +) + +func Test_ParseAutoRestConfig(t *testing.T) { + wd, _ := os.Getwd() + testcases := []struct { + Input string + Expected []Package + }{ + { + Input: path.Join(wd, "testdata", "readme.md"), + Expected: []Package{ + { + Tag: "package-2015-10", + }, + { + Tag: "package-2017-05-preview", + }, + { + Tag: "package-2018-01-preview", + }, + { + Tag: "package-2018-06-preview", + }, + { + Tag: "package-2019-06", + }, + { + Tag: "package-2020-01-13-preview", + }, + { + Tag: "package-2021-06-22", + }, + { + Tag: "package-2022-01-31", + }, + { + Tag: "package-2022-02-22", + }, + { + Tag: "package-2022-08-08", + }, + }, + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] testcase: %+v", testcase.Input) + actual := ParseAutoRestConfig(testcase.Input) + if len(actual) != len(testcase.Expected) { + t.Errorf("expected %d packages, got %d", len(testcase.Expected), len(actual)) + } + for i := range actual { + if actual[i].Tag != testcase.Expected[i].Tag { + t.Errorf("expected %s, got %s", testcase.Expected[i].Tag, actual[i].Tag) + } + if len(actual[i].InputFiles) == 0 { + t.Errorf("expected non-empty input files") + } + } + } +} + +func Test_ParseYamlConfig(t *testing.T) { + testcases := []struct { + Input string + Expected *Package + ExpectError bool + }{ + { + Input: `$(tag) == 'package-2015-10' +input-file: +- Microsoft.Automation/stable/2015-10-31/account.json +- Microsoft.Automation/stable/2015-10-31/certificate.json +`, + Expected: &Package{ + Tag: "package-2015-10", + InputFiles: []string{ + "Microsoft.Automation/stable/2015-10-31/account.json", + "Microsoft.Automation/stable/2015-10-31/certificate.json", + }, + }, + ExpectError: false, + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] testcase: %+v", testcase.Input) + + actual, err := ParseYamlConfig(testcase.Input) + if testcase.ExpectError != (err != nil) { + t.Errorf("expected error %v, got %v", testcase.ExpectError, err) + continue + } + if actual.Tag != testcase.Expected.Tag { + t.Errorf("expected %s, got %s", testcase.Expected.Tag, actual.Tag) + } + if len(actual.InputFiles) != len(testcase.Expected.InputFiles) { + t.Errorf("expected %d input files, got %d", len(testcase.Expected.InputFiles), len(actual.InputFiles)) + continue + } + for i := range actual.InputFiles { + if actual.InputFiles[i] != testcase.Expected.InputFiles[i] { + t.Errorf("expected %s, got %s", testcase.Expected.InputFiles[i], actual.InputFiles[i]) + } + } + } + +} diff --git a/autorest/testdata/readme.md b/autorest/testdata/readme.md new file mode 100644 index 00000000..520aadbe --- /dev/null +++ b/autorest/testdata/readme.md @@ -0,0 +1,620 @@ +# Automation + +> see https://aka.ms/autorest + +This is the AutoRest configuration file for Automation. + +--- +## Getting Started +To build the SDK for Automation, simply [Install AutoRest](https://aka.ms/autorest/install) and in this folder, run: + +> `autorest` + +To see additional help and options, run: + +> `autorest --help` +--- + +## Configuration + +======= +### Basic Information +These are the global settings for the Automation API. + +``` yaml +title: AutomationClient +description: Automation Client +openapi-type: arm +tag: package-2022-08-08 +``` + +### Tag: package-2015-10 + +These settings apply only when `--tag=package-2015-10` is specified on the command line. + +``` yaml $(tag) == 'package-2015-10' +input-file: +- Microsoft.Automation/stable/2015-10-31/account.json +- Microsoft.Automation/stable/2015-10-31/certificate.json +- Microsoft.Automation/stable/2015-10-31/connection.json +- Microsoft.Automation/stable/2015-10-31/connectionType.json +- Microsoft.Automation/stable/2015-10-31/credential.json +- Microsoft.Automation/stable/2015-10-31/dscCompilationJob.json +- Microsoft.Automation/stable/2015-10-31/dscConfiguration.json +- Microsoft.Automation/stable/2015-10-31/dscNode.json +- Microsoft.Automation/stable/2015-10-31/dscNodeConfiguration.json +- Microsoft.Automation/stable/2015-10-31/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2015-10-31/job.json +- Microsoft.Automation/stable/2015-10-31/jobSchedule.json +- Microsoft.Automation/stable/2015-10-31/linkedWorkspace.json +- Microsoft.Automation/stable/2015-10-31/module.json +- Microsoft.Automation/stable/2015-10-31/runbook.json +- Microsoft.Automation/stable/2015-10-31/schedule.json +- Microsoft.Automation/stable/2015-10-31/variable.json +- Microsoft.Automation/stable/2015-10-31/watcher.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +``` + + +### Tag: package-2017-05-preview + +These settings apply only when `--tag=package-2017-05-preview` is specified on the command line. + +``` yaml $(tag) == 'package-2017-05-preview' +input-file: +- Microsoft.Automation/stable/2015-10-31/account.json +- Microsoft.Automation/stable/2015-10-31/certificate.json +- Microsoft.Automation/stable/2015-10-31/connection.json +- Microsoft.Automation/stable/2015-10-31/connectionType.json +- Microsoft.Automation/stable/2015-10-31/credential.json +- Microsoft.Automation/stable/2015-10-31/dscCompilationJob.json +- Microsoft.Automation/stable/2015-10-31/dscConfiguration.json +- Microsoft.Automation/stable/2015-10-31/dscNode.json +- Microsoft.Automation/stable/2015-10-31/dscNodeConfiguration.json +- Microsoft.Automation/stable/2015-10-31/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2015-10-31/jobSchedule.json +- Microsoft.Automation/stable/2015-10-31/linkedWorkspace.json +- Microsoft.Automation/stable/2015-10-31/module.json +- Microsoft.Automation/stable/2015-10-31/runbook.json +- Microsoft.Automation/stable/2015-10-31/schedule.json +- Microsoft.Automation/stable/2015-10-31/variable.json +- Microsoft.Automation/stable/2015-10-31/watcher.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfiguration.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationRun.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControl.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/preview/2017-05-15-preview/job.json +``` + +### Tag: package-2018-01-preview + +These settings apply only when `--tag=package-2018-01-preview` is specified on the command line. + +``` yaml $(tag) == 'package-2018-01-preview' +input-file: +- Microsoft.Automation/stable/2015-10-31/account.json +- Microsoft.Automation/stable/2015-10-31/certificate.json +- Microsoft.Automation/stable/2015-10-31/connection.json +- Microsoft.Automation/stable/2015-10-31/connectionType.json +- Microsoft.Automation/stable/2015-10-31/credential.json +- Microsoft.Automation/stable/2015-10-31/dscConfiguration.json +- Microsoft.Automation/stable/2015-10-31/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2015-10-31/jobSchedule.json +- Microsoft.Automation/stable/2015-10-31/linkedWorkspace.json +- Microsoft.Automation/stable/2015-10-31/module.json +- Microsoft.Automation/stable/2015-10-31/runbook.json +- Microsoft.Automation/stable/2015-10-31/schedule.json +- Microsoft.Automation/stable/2015-10-31/variable.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2015-10-31/watcher.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfiguration.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationRun.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControl.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/preview/2017-05-15-preview/job.json +- Microsoft.Automation/stable/2018-01-15/dscNode.json +- Microsoft.Automation/stable/2018-01-15/dscCompilationJob.json +- Microsoft.Automation/stable/2018-01-15/dscNodeConfiguration.json +- Microsoft.Automation/stable/2018-01-15/dscNodeCounts.json +``` + +### Tag: package-2018-06-preview + +These settings apply only when `--tag=package-2018-06-preview` is specified on the command line. + +``` yaml $(tag) == 'package-2018-06-preview' +input-file: +- Microsoft.Automation/stable/2015-10-31/account.json +- Microsoft.Automation/stable/2015-10-31/certificate.json +- Microsoft.Automation/stable/2015-10-31/connection.json +- Microsoft.Automation/stable/2015-10-31/connectionType.json +- Microsoft.Automation/stable/2015-10-31/credential.json +- Microsoft.Automation/stable/2015-10-31/dscConfiguration.json +- Microsoft.Automation/stable/2015-10-31/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2015-10-31/jobSchedule.json +- Microsoft.Automation/stable/2015-10-31/linkedWorkspace.json +- Microsoft.Automation/stable/2015-10-31/module.json +- Microsoft.Automation/stable/2015-10-31/schedule.json +- Microsoft.Automation/stable/2015-10-31/variable.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2015-10-31/watcher.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfiguration.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationRun.json +- Microsoft.Automation/preview/2017-05-15-preview/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControl.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2017-05-15-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/preview/2017-05-15-preview/job.json +- Microsoft.Automation/stable/2018-01-15/dscNode.json +- Microsoft.Automation/stable/2018-01-15/dscCompilationJob.json +- Microsoft.Automation/stable/2018-01-15/dscNodeConfiguration.json +- Microsoft.Automation/stable/2018-01-15/dscNodeCounts.json +- Microsoft.Automation/stable/2018-06-30/runbook.json +- Microsoft.Automation/stable/2018-06-30/python2package.json +``` + +### Tag: package-2019-06 + +These settings apply only when `--tag=package-2019-06` is specified on the command line. + +``` yaml $(tag) == 'package-2019-06' +input-file: +- Microsoft.Automation/stable/2019-06-01/runbook.json +- Microsoft.Automation/stable/2019-06-01/python2package.json +- Microsoft.Automation/stable/2019-06-01/dscNode.json +- Microsoft.Automation/stable/2019-06-01/dscCompilationJob.json +- Microsoft.Automation/stable/2019-06-01/dscNodeConfiguration.json +- Microsoft.Automation/stable/2019-06-01/dscNodeCounts.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2019-06-01/sourceControl.json +- Microsoft.Automation/stable/2019-06-01/sourceControlSyncJob.json +- Microsoft.Automation/stable/2019-06-01/sourceControlSyncJobStreams.json +- Microsoft.Automation/stable/2019-06-01/job.json +- Microsoft.Automation/stable/2019-06-01/account.json +- Microsoft.Automation/stable/2019-06-01/certificate.json +- Microsoft.Automation/stable/2019-06-01/connection.json +- Microsoft.Automation/stable/2019-06-01/connectionType.json +- Microsoft.Automation/stable/2019-06-01/credential.json +- Microsoft.Automation/stable/2019-06-01/dscConfiguration.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2019-06-01/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2019-06-01/jobSchedule.json +- Microsoft.Automation/stable/2019-06-01/linkedWorkspace.json +- Microsoft.Automation/stable/2019-06-01/module.json +- Microsoft.Automation/stable/2019-06-01/operations.json +- Microsoft.Automation/stable/2019-06-01/schedule.json +- Microsoft.Automation/stable/2019-06-01/variable.json +- Microsoft.Automation/stable/2019-06-01/watcher.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +``` + +### Tag: package-2020-01-13-preview + +These settings apply only when `--tag=package-2020-01-13-preview` is specified on the command line. + +``` yaml $(tag) == 'package-2020-01-13-preview' +input-file: +- Microsoft.Automation/preview/2020-01-13-preview/privateEndpointConnection.json +- Microsoft.Automation/preview/2020-01-13-preview/privateLinkResources.json +- Microsoft.Automation/preview/2020-01-13-preview/python2package.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNode.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeConfiguration.json +- Microsoft.Automation/preview/2020-01-13-preview/dscCompilationJob.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeCounts.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControl.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/preview/2020-01-13-preview/account.json +- Microsoft.Automation/preview/2020-01-13-preview/certificate.json +- Microsoft.Automation/preview/2020-01-13-preview/connection.json +- Microsoft.Automation/preview/2020-01-13-preview/connectionType.json +- Microsoft.Automation/preview/2020-01-13-preview/credential.json +- Microsoft.Automation/preview/2020-01-13-preview/hybridRunbookWorkerGroup.json +- Microsoft.Automation/preview/2020-01-13-preview/jobSchedule.json +- Microsoft.Automation/preview/2020-01-13-preview/linkedWorkspace.json +- Microsoft.Automation/preview/2020-01-13-preview/module.json +- Microsoft.Automation/preview/2020-01-13-preview/schedule.json +- Microsoft.Automation/preview/2020-01-13-preview/variable.json +- Microsoft.Automation/preview/2020-01-13-preview/watcher.json +- Microsoft.Automation/stable/2019-06-01/dscConfiguration.json +- Microsoft.Automation/stable/2019-06-01/job.json +- Microsoft.Automation/stable/2019-06-01/operations.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2018-06-30/runbook.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +``` + +### Tag: package-2021-06-22 + +These settings apply only when `--tag=package-2021-06-22` is specified on the command line. + +``` yaml $(tag) == 'package-2021-06-22' +input-file: +- Microsoft.Automation/preview/2020-01-13-preview/privateEndpointConnection.json +- Microsoft.Automation/preview/2020-01-13-preview/privateLinkResources.json +- Microsoft.Automation/preview/2020-01-13-preview/python2package.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNode.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeConfiguration.json +- Microsoft.Automation/preview/2020-01-13-preview/dscCompilationJob.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeCounts.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControl.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/stable/2021-06-22/account.json +- Microsoft.Automation/preview/2020-01-13-preview/certificate.json +- Microsoft.Automation/preview/2020-01-13-preview/connection.json +- Microsoft.Automation/preview/2020-01-13-preview/connectionType.json +- Microsoft.Automation/preview/2020-01-13-preview/credential.json +- Microsoft.Automation/stable/2021-06-22/hybridRunbookWorkerGroup.json +- Microsoft.Automation/preview/2020-01-13-preview/jobSchedule.json +- Microsoft.Automation/preview/2020-01-13-preview/linkedWorkspace.json +- Microsoft.Automation/preview/2020-01-13-preview/module.json +- Microsoft.Automation/preview/2020-01-13-preview/schedule.json +- Microsoft.Automation/preview/2020-01-13-preview/variable.json +- Microsoft.Automation/preview/2020-01-13-preview/watcher.json +- Microsoft.Automation/stable/2019-06-01/dscConfiguration.json +- Microsoft.Automation/stable/2019-06-01/job.json +- Microsoft.Automation/stable/2021-06-22/operations.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2018-06-30/runbook.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2021-06-22/hybridRunbookWorker.json +``` + +### Tag: package-2022-01-31 + +These settings apply only when `--tag=package-2022-01-31` is specified on the command line. + +``` yaml $(tag) == 'package-2022-01-31' +input-file: +- Microsoft.Automation/preview/2020-01-13-preview/privateEndpointConnection.json +- Microsoft.Automation/preview/2020-01-13-preview/privateLinkResources.json +- Microsoft.Automation/preview/2020-01-13-preview/python2package.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNode.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeConfiguration.json +- Microsoft.Automation/preview/2020-01-13-preview/dscCompilationJob.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeCounts.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControl.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/stable/2021-06-22/account.json +- Microsoft.Automation/preview/2020-01-13-preview/certificate.json +- Microsoft.Automation/preview/2020-01-13-preview/connection.json +- Microsoft.Automation/preview/2020-01-13-preview/connectionType.json +- Microsoft.Automation/preview/2020-01-13-preview/credential.json +- Microsoft.Automation/stable/2021-06-22/hybridRunbookWorkerGroup.json +- Microsoft.Automation/preview/2020-01-13-preview/jobSchedule.json +- Microsoft.Automation/preview/2020-01-13-preview/linkedWorkspace.json +- Microsoft.Automation/preview/2020-01-13-preview/module.json +- Microsoft.Automation/preview/2020-01-13-preview/schedule.json +- Microsoft.Automation/preview/2020-01-13-preview/variable.json +- Microsoft.Automation/preview/2020-01-13-preview/watcher.json +- Microsoft.Automation/stable/2019-06-01/dscConfiguration.json +- Microsoft.Automation/stable/2019-06-01/job.json +- Microsoft.Automation/stable/2021-06-22/operations.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2018-06-30/runbook.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2021-06-22/hybridRunbookWorker.json +- Microsoft.Automation/stable/2022-01-31/deletedAutomationAccount.json +``` + +### Tag: package-2022-02-22 + +These settings apply only when `--tag=package-2022-02-22` is specified on the command line. + +``` yaml $(tag) == 'package-2022-02-22' +input-file: +- Microsoft.Automation/preview/2020-01-13-preview/privateEndpointConnection.json +- Microsoft.Automation/preview/2020-01-13-preview/privateLinkResources.json +- Microsoft.Automation/preview/2020-01-13-preview/python2package.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNode.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeConfiguration.json +- Microsoft.Automation/preview/2020-01-13-preview/dscCompilationJob.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeCounts.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControl.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJob.json +- Microsoft.Automation/preview/2020-01-13-preview/sourceControlSyncJobStreams.json +- Microsoft.Automation/stable/2021-06-22/account.json +- Microsoft.Automation/preview/2020-01-13-preview/certificate.json +- Microsoft.Automation/preview/2020-01-13-preview/connection.json +- Microsoft.Automation/preview/2020-01-13-preview/connectionType.json +- Microsoft.Automation/preview/2020-01-13-preview/credential.json +- Microsoft.Automation/preview/2020-01-13-preview/jobSchedule.json +- Microsoft.Automation/preview/2020-01-13-preview/linkedWorkspace.json +- Microsoft.Automation/preview/2020-01-13-preview/module.json +- Microsoft.Automation/preview/2020-01-13-preview/schedule.json +- Microsoft.Automation/preview/2020-01-13-preview/variable.json +- Microsoft.Automation/preview/2020-01-13-preview/watcher.json +- Microsoft.Automation/stable/2019-06-01/dscConfiguration.json +- Microsoft.Automation/stable/2019-06-01/job.json +- Microsoft.Automation/stable/2021-06-22/operations.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2018-06-30/runbook.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2021-06-22/hybridRunbookWorker.json +- Microsoft.Automation/stable/2022-01-31/deletedAutomationAccount.json +- Microsoft.Automation/stable/2022-02-22/hybridRunbookWorkerGroup.json +``` + +### Tag: package-2022-08-08 + +These settings apply only when `--tag=package-2022-08-08` is specified on the command line. + +``` yaml $(tag) == 'package-2022-08-08' +input-file: +- Microsoft.Automation/preview/2020-01-13-preview/privateEndpointConnection.json +- Microsoft.Automation/preview/2020-01-13-preview/privateLinkResources.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNode.json +- Microsoft.Automation/preview/2020-01-13-preview/dscCompilationJob.json +- Microsoft.Automation/preview/2020-01-13-preview/dscNodeCounts.json +- Microsoft.Automation/preview/2020-01-13-preview/watcher.json +- Microsoft.Automation/stable/2019-06-01/softwareUpdateConfiguration.json +- Microsoft.Automation/stable/2015-10-31/webhook.json +- Microsoft.Automation/stable/2022-01-31/deletedAutomationAccount.json +- Microsoft.Automation/stable/2022-08-08/account.json +- Microsoft.Automation/stable/2022-08-08/certificate.json +- Microsoft.Automation/stable/2022-08-08/connection.json +- Microsoft.Automation/stable/2022-08-08/connectionType.json +- Microsoft.Automation/stable/2022-08-08/credential.json +- Microsoft.Automation/stable/2022-08-08/dscConfiguration.json +- Microsoft.Automation/stable/2022-08-08/dscNodeConfiguration.json +- Microsoft.Automation/stable/2022-08-08/hybridRunbookWorker.json +- Microsoft.Automation/stable/2022-08-08/hybridRunbookWorkerGroup.json +- Microsoft.Automation/stable/2022-08-08/job.json +- Microsoft.Automation/stable/2022-08-08/jobSchedule.json +- Microsoft.Automation/stable/2022-08-08/linkedWorkspace.json +- Microsoft.Automation/stable/2022-08-08/module.json +- Microsoft.Automation/stable/2022-08-08/operations.json +- Microsoft.Automation/stable/2022-08-08/python2package.json +- Microsoft.Automation/stable/2022-08-08/python3package.json +- Microsoft.Automation/stable/2022-08-08/runbook.json +- Microsoft.Automation/stable/2022-08-08/schedule.json +- Microsoft.Automation/stable/2022-08-08/softwareUpdateConfigurationMachineRun.json +- Microsoft.Automation/stable/2022-08-08/softwareUpdateConfigurationRun.json +- Microsoft.Automation/stable/2022-08-08/sourceControl.json +- Microsoft.Automation/stable/2022-08-08/sourceControlSyncJob.json +- Microsoft.Automation/stable/2022-08-08/sourceControlSyncJobStreams.json +- Microsoft.Automation/stable/2022-08-08/variable.json +``` + +--- +## Suppression + +sdf + +``` yaml +directive: + - suppress: RequiredPropertiesMissingInResourceModel + from: runbook.json + where: $.definitions.TestJob + - suppress: BodyTopLevelProperties + from: runbook.json + - suppress: DefinitionsPropertiesNamesCamelCase + from: account.json + where: $.definitions.Key.properties.KeyName + - suppress: DefinitionsPropertiesNamesCamelCase + from: account.json + where: $.definitions.Key.properties.Permissions + - suppress: DefinitionsPropertiesNamesCamelCase + from: account.json + where: $.definitions.Key.properties.Value + - suppress: LongRunningResponseStatusCode + from: runbook.json + where: $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/runbooks/{runbookName}/draft/publish"].post["x-ms-long-running-operation"] + - suppress: LongRunningResponseStatusCode + from: runbook.json + where: $.paths["/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/runbooks/{runbookName}/publish"].post["x-ms-long-running-operation"] + - suppress: DefaultErrorResponseSchema + from: hybridRunbookWorkerGroup.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: hybridRunbookWorker.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: operations.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: BodyTopLevelProperties + from: hybridRunbookWorkerGroup.json + reason: This body format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: RequiredPropertiesMissingInResourceModel + from: hybridRunbookWorkerGroup.json + reason: This body format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: deletedAutomationAccount.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: python3package.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: account.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: certificate.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: connection.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: connectionType.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: credential.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: dscConfiguration.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: dscNodeConfiguration.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: job.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: jobSchedule.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: linkedWorkspace.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: module.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: python2package.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: runbook.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: schedule.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: softwareUpdateConfigurationMachineRun.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: softwareUpdateConfigurationRun.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: sourceControl.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: sourceControlSyncJob.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: sourceControlSyncJobStreams.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: variable.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DefaultErrorResponseSchema + from: runbook.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: credential.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: certificate.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: dscNodeConfiguration.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: hybridRunbookWorkerGroup.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: module.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: python2package.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: schedule.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: sourceControl.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: variable.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. + - suppress: DeleteOperationResponses + from: python2package.json + reason: This error format is already part of the previous api, cannot change it as it will result in breaking change. +``` + +--- +# Code Generation + +## Swagger to SDK + +This section describes what SDK should be generated by the automatic system. +This is not used by Autorest itself. + +``` yaml $(swagger-to-sdk) +swagger-to-sdk: + - repo: azure-sdk-for-net-track2 + - repo: azure-sdk-for-python-track2 + - repo: azure-sdk-for-go + - repo: azure-sdk-for-js + - repo: azure-sdk-for-node + - repo: azure-sdk-for-ruby + after_scripts: + - bundle install && rake arm:regen_all_profiles['azure_mgmt_automation'] + - repo: azure-resource-manager-schemas + - repo: azure-powershell +``` + + +## C# + +These settings apply only when `--csharp` is specified on the command line. +Please also specify `--csharp-sdks-folder=`. + +``` yaml $(csharp) +csharp: + azure-arm: true + license-header: MICROSOFT_MIT_NO_VERSION + namespace: Microsoft.Azure.Management.Automation + output-folder: $(csharp-sdks-folder)/automation/Microsoft.Azure.Management.Automation/src/Generated + clear-output-folder: true +``` + +## Python + +See configuration in [readme.python.md](./readme.python.md) + +## Go + +See configuration in [readme.go.md](./readme.go.md) + +## Java + +These settings apply only when `--java` is specified on the command line. +Please also specify `--azure-libraries-for-java-folder=`. + +``` yaml $(java) +azure-arm: true +fluent: true +namespace: com.microsoft.azure.management.automation +license-header: MICROSOFT_MIT_NO_CODEGEN +payload-flattening-threshold: 1 +output-folder: $(azure-libraries-for-java-folder)/azure-mgmt-automation +``` + +### Java multi-api + +``` yaml $(java) && $(multiapi) +batch: + - tag: package-2015-10 +``` + +### Tag: package-2015-10 and java + +These settings apply only when `--tag=package-2015-10 --java` is specified on the command line. +Please also specify `--azure-libraries-for-java=`. + +``` yaml $(tag) == 'package-2015-10' && $(java) && $(multiapi) +java: + namespace: com.microsoft.azure.management.automation.v2015_10_31 + output-folder: $(azure-libraries-for-java-folder)/sdk/automation/mgmt-v2015_10_31 +regenerate-manager: true +generate-interface: true +``` + + + + + diff --git a/commands/auto.go b/commands/auto.go deleted file mode 100644 index d7c3848c..00000000 --- a/commands/auto.go +++ /dev/null @@ -1,84 +0,0 @@ -package commands - -import ( - "flag" - "fmt" - "log" - "strings" - - "github.com/mitchellh/cli" -) - -type AutoCommand struct { - Ui cli.Ui - path string - workingDir string - verbose bool - useRawJsonPayload bool - overwrite bool -} - -func (c *AutoCommand) flags() *flag.FlagSet { - fs := defaultFlagSet("auto") - fs.StringVar(&c.path, "path", "", "filepath of rest api to create arm resource example") - fs.StringVar(&c.workingDir, "working-dir", "", "output path to Terraform configuration files") - fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") - fs.BoolVar(&c.useRawJsonPayload, "raw", false, "whether use raw json payload in 'body'") - fs.BoolVar(&c.overwrite, "overwrite", false, "whether overwrite existing terraform configurations") - fs.Usage = func() { c.Ui.Error(c.Help()) } - return fs -} -func (c AutoCommand) Help() string { - helpText := ` -Usage: armstrong auto -path [-v] [-working-dir ] -` + c.Synopsis() + "\n\n" + helpForFlags(c.flags()) - - return strings.TrimSpace(helpText) -} - -func (c AutoCommand) Synopsis() string { - return "Run generate and test, if test passed, run cleanup" -} - -func (c AutoCommand) Run(args []string) int { - f := c.flags() - if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) - return 1 - } - if len(c.path) == 0 { - c.Ui.Error(c.Help()) - return 1 - } - return c.Execute() -} - -func (c AutoCommand) Execute() int { - result := GenerateCommand{ - Ui: c.Ui, - workingDir: c.workingDir, - path: c.path, - overwrite: c.overwrite, - useRawJsonPayload: c.useRawJsonPayload, - }.Execute() - if result != 0 { - log.Println("[ERROR] Generate failed, skip test") - return result - } - result = TestCommand{ - Ui: c.Ui, - verbose: c.verbose, - workingDir: c.workingDir, - }.Execute() - if result != 0 { - log.Println("[ERROR] Test failed, skip cleanup") - return result - } - CleanupCommand{ - Ui: c.Ui, - verbose: c.verbose, - workingDir: c.workingDir, - }.Execute() - log.Println("[INFO] Test passed!") - return 0 -} diff --git a/commands/cleanup.go b/commands/cleanup.go index 4aaa315d..f3d07a9d 100644 --- a/commands/cleanup.go +++ b/commands/cleanup.go @@ -3,21 +3,19 @@ package commands import ( "flag" "fmt" - "log" "os" "path" "path/filepath" "strings" "time" - "github.com/mitchellh/cli" "github.com/ms-henglu/armstrong/report" "github.com/ms-henglu/armstrong/tf" "github.com/ms-henglu/armstrong/types" + "github.com/sirupsen/logrus" ) type CleanupCommand struct { - Ui cli.Ui verbose bool workingDir string } @@ -26,7 +24,7 @@ func (c *CleanupCommand) flags() *flag.FlagSet { fs := defaultFlagSet("cleanup") fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") fs.StringVar(&c.workingDir, "working-dir", "", "path to Terraform configuration files") - fs.Usage = func() { c.Ui.Error(c.Help()) } + fs.Usage = func() { logrus.Error(c.Help()) } return fs } @@ -45,9 +43,12 @@ func (c CleanupCommand) Synopsis() string { func (c CleanupCommand) Run(args []string) int { f := c.flags() if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) + logrus.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) return 1 } + if c.verbose { + logrus.SetLevel(logrus.DebugLevel) + } return c.Execute() } @@ -57,27 +58,27 @@ func (c CleanupCommand) Execute() int { partialPassedReportFileName = "cleanup_partial_passed_report.md" ) - log.Println("[INFO] ----------- cleanup resources ---------") + logrus.Infof("cleaning up resources...") wd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("failed to get working directory: %+v", err)) + logrus.Error(fmt.Sprintf("failed to get working directory: %+v", err)) return 1 } if c.workingDir != "" { wd, err = filepath.Abs(c.workingDir) if err != nil { - c.Ui.Error(fmt.Sprintf("working directory is invalid: %+v", err)) + logrus.Error(fmt.Sprintf("working directory is invalid: %+v", err)) return 1 } } terraform, err := tf.NewTerraform(wd, c.verbose) if err != nil { - log.Fatalf("[ERROR] error creating terraform executable: %+v\n", err) + logrus.Fatalf("creating terraform executable: %+v", err) } state, err := terraform.Show() if err != nil { - log.Fatalf("[ERROR] error getting state: %+v\n", err) + logrus.Fatalf("failed to get terraform state: %+v", err) } passReport := tf.NewPassReportFromState(state) @@ -89,23 +90,23 @@ func (c CleanupCommand) Execute() int { reportDir = path.Join(wd, reportDir) err = os.Mkdir(reportDir, 0755) if err != nil { - log.Fatalf("[ERROR] error creating report dir %s: %+v", reportDir, err) + logrus.Fatalf("failed to create report directory: %+v", err) } - log.Println("[INFO] prepare working directory") + logrus.Infof("running terraform init...") _ = terraform.Init() - log.Println("[INFO] running destroy command to cleanup resources...") + logrus.Infof("running terraform destroy...") destroyErr := terraform.Destroy() if destroyErr != nil { - log.Printf("[ERROR] error cleaning up resources: %+v\n", destroyErr) + logrus.Errorf("failed to destroy resources: %+v", destroyErr) } else { - log.Println("[INFO] all resources are cleaned up") + logrus.Infof("all resources are cleaned up") storeCleanupReport(passReport, reportDir, allPassedReportFileName) } logs, err := report.ParseLogs(path.Join(wd, "log.txt")) if err != nil { - log.Printf("[ERROR] parsing log.txt: %+v", err) + logrus.Errorf("failed to parse log.txt: %+v", err) } errorReport := types.ErrorReport{} @@ -140,10 +141,10 @@ func (c CleanupCommand) Execute() int { storeCleanupReport(passReport, reportDir, partialPassedReportFileName) } - log.Println("[INFO] ---------------- Summary ----------------") - log.Printf("[INFO] %d resources passed the cleanup tests.", len(passReport.Resources)) + logrus.Infof("---------------- Summary ----------------") + logrus.Infof("%d resources passed the cleanup tests.", len(passReport.Resources)) if len(errorReport.Errors) != 0 { - log.Printf("[INFO] %d errors when cleanup the testing resources.", len(errorReport.Errors)) + logrus.Infof("%d errors when cleanup the testing resources.", len(errorReport.Errors)) } return 0 @@ -153,22 +154,22 @@ func storeCleanupReport(passReport types.PassReport, reportDir string, reportNam if len(passReport.Resources) != 0 { err := os.WriteFile(path.Join(reportDir, reportName), []byte(report.CleanupMarkdownReport(passReport)), 0644) if err != nil { - log.Printf("[WARN] failed to save passed markdown report to %s: %+v", reportName, err) + logrus.Errorf("failed to save passed markdown report to %s: %+v", reportName, err) } else { - log.Printf("[INFO] markdown report saved to %s", reportName) + logrus.Infof("markdown report saved to %s", reportName) } } } func storeCleanupErrorReport(errorReport types.ErrorReport, reportDir string) { for _, r := range errorReport.Errors { - log.Printf("[WARN] found an error when deleting %s, address: %s\n", r.Type, r.Label) + logrus.Warnf("found an error when deleting %s, address: %s\n", r.Type, r.Label) markdownFilename := fmt.Sprintf("%s_%s.md", strings.ReplaceAll(r.Type, "/", "_"), r.Label) err := os.WriteFile(path.Join(reportDir, markdownFilename), []byte(report.CleanupErrorMarkdownReport(r, errorReport.Logs)), 0644) if err != nil { - log.Printf("[WARN] failed to save markdown report to %s: %+v", markdownFilename, err) + logrus.Errorf("failed to save markdown report to %s: %+v", markdownFilename, err) } else { - log.Printf("[INFO] markdown report saved to %s", markdownFilename) + logrus.Infof("markdown report saved to %s", markdownFilename) } } } diff --git a/commands/generate.go b/commands/generate.go index d23a0ba6..25561115 100644 --- a/commands/generate.go +++ b/commands/generate.go @@ -3,45 +3,73 @@ package commands import ( "flag" "fmt" - "log" "os" "path" "path/filepath" + "sort" "strings" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/mitchellh/cli" - "github.com/ms-henglu/armstrong/hcl" - "github.com/ms-henglu/armstrong/loader" + "github.com/ms-henglu/armstrong/autorest" "github.com/ms-henglu/armstrong/resource" - "github.com/ms-henglu/armstrong/types" + "github.com/ms-henglu/armstrong/resource/resolver" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/swagger" + "github.com/ms-henglu/armstrong/tf" + "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) type GenerateCommand struct { - Ui cli.Ui - path string + // common options + verbose bool workingDir string useRawJsonPayload bool - overwrite bool - resourceType string + + // create with example path + path string + resourceType string + overwrite bool + + // create with swagger path + swaggerPath string + + // create with autorest config, TODO: remove them? because the tag contains swaggers from different api-versions + readmePath string + tag string } func (c *GenerateCommand) flags() *flag.FlagSet { fs := defaultFlagSet("generate") - fs.StringVar(&c.path, "path", "", "path to a swagger 'Create' example") + // common options fs.StringVar(&c.workingDir, "working-dir", "", "output path to Terraform configuration files") fs.BoolVar(&c.useRawJsonPayload, "raw", false, "whether use raw json payload in 'body'") - fs.BoolVar(&c.overwrite, "overwrite", false, "whether overwrite existing terraform configurations") + fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") + + // generate with example options + fs.StringVar(&c.path, "path", "", "path to a swagger 'Create' example") fs.StringVar(&c.resourceType, "type", "resource", "the type of the resource to be generated, allowed values: 'resource'(supports CRUD) and 'data'(read-only). Defaults to 'resource'") - fs.Usage = func() { c.Ui.Error(c.Help()) } + fs.BoolVar(&c.overwrite, "overwrite", false, "whether overwrite existing terraform configurations") + + // generate with swagger options + fs.StringVar(&c.swaggerPath, "swagger", "", "path or directory to swagger.json files") + + // generate with autorest config + fs.StringVar(&c.readmePath, "readme", "", "path to the autorest config file(readme.md)") + fs.StringVar(&c.tag, "tag", "", "tag in the autorest config file(readme.md)") + + fs.Usage = func() { logrus.Error(c.Help()) } return fs } func (c GenerateCommand) Help() string { helpText := ` -Usage: armstrong generate -path [-working-dir ] +Usage: + armstrong generate -path [-working-dir ] + armstrong generate -swagger [-working-dir ] ` + c.Synopsis() + "\n\n" + helpForFlags(c.flags()) return strings.TrimSpace(helpText) @@ -52,13 +80,30 @@ func (c GenerateCommand) Synopsis() string { } func (c GenerateCommand) Run(args []string) int { + logrus.Debugf("args: %+v", args) f := c.flags() if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %+v", err)) + logrus.Errorf("Error parsing command-line flags: %+v", err) + return 1 + } + logrus.Debugf("flags: %+v", f) + if c.verbose { + logrus.SetLevel(logrus.DebugLevel) + } + if c.swaggerPath != "" && c.path != "" && c.readmePath != "" { + logrus.Error("only one of 'swagger', 'path' and 'readme' can be specified") return 1 } - if len(c.path) == 0 { - c.Ui.Error(c.Help()) + if c.path == "" && c.swaggerPath == "" && c.readmePath == "" { + logrus.Error(c.Help()) + return 1 + } + if c.readmePath != "" && c.tag == "" { + logrus.Error("tag must be specified when 'readme' is specified") + return 1 + } + if c.readmePath == "" && c.tag != "" { + logrus.Errorf("tag can only be specified when 'readme' is specified") return 1 } return c.Execute() @@ -67,112 +112,347 @@ func (c GenerateCommand) Run(args []string) int { func (c GenerateCommand) Execute() int { wd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("failed to get working directory: %+v", err)) + logrus.Errorf("Error getting working directory: %+v", err) return 1 } if c.workingDir != "" { wd, err = filepath.Abs(c.workingDir) if err != nil { - c.Ui.Error(fmt.Sprintf("working directory is invalid: %+v", err)) + logrus.Errorf("Error getting absolute path of working directory: %+v", err) return 1 } } + c.workingDir = wd + logrus.Infof("working directory: %s", c.workingDir) + + switch { + case c.swaggerPath != "": + return c.fromSwaggerPath() + case c.path != "": + return c.fromExamplePath() + case c.readmePath != "": + return c.fromAutorestConfig() + } + + // should not reach here + logrus.Println(c.Help()) + return 1 +} + +func (c GenerateCommand) fromExamplePath() int { + wd := c.workingDir if c.overwrite { + logrus.Infof("overwriting existing terraform configurations...") _ = os.RemoveAll(path.Join(wd, "testing.tf")) _ = os.RemoveAll(path.Join(wd, "dependency.tf")) } - err = os.WriteFile(path.Join(wd, "provider.tf"), hclwrite.Format([]byte(hcl.ProviderHcl)), 0644) + err := os.WriteFile(path.Join(wd, "provider.tf"), hclwrite.Format([]byte(resource.DefaultProviderConfig)), 0644) + if err != nil { + logrus.Errorf("writing provider.tf: %+v", err) + } + logrus.Infof("provider configuration is written to %s", path.Join(wd, "provider.tf")) + + // load example + logrus.Infof("loading example: %s", c.path) + example, err := resource.NewAzapiDefinitionFromExample(c.path, c.resourceType) if err != nil { - log.Fatalf("[ERROR] error writing provider.tf: %+v\n", err) + logrus.Fatalf("loading example: %+v", err) + } + if c.useRawJsonPayload { + logrus.Infof("using raw json payload in 'body'...") + example.BodyFormat = types.BodyFormatJson } - log.Println("[INFO] ----------- generate dependencies and testing resource ---------") // load dependencies - log.Println("[INFO] loading dependencies") - existDeps, deps := loadDependencies(wd) + logrus.Infof("loading dependencies...") + referenceResolvers := []resolver.ReferenceResolver{ + resolver.NewExistingDependencyResolver(wd), + resolver.NewAzapiDependencyResolver(), + resolver.NewAzurermDependencyResolver(), + resolver.NewProviderIDResolver(), + resolver.NewLocationIDResolver(), + resolver.NewAzapiResourcePlaceholderResolver(), + } + context := resource.NewContext(referenceResolvers) + context.InitFile(allTerraformConfig(wd)) - // load example - log.Println("[INFO] generating testing files") - exampleFilepath := c.path - var base resource.Base - if c.resourceType == "data" { - base, err = resource.NewDataSourceFromExample(exampleFilepath) + logrus.Infof("generating terraform configurations...") + err = context.AddAzapiDefinition(example) + if err != nil { + return 0 + } + + logrus.Infof("writing terraform configurations...") + blockMap := blockFileMap(wd) + contentToAppend := make(map[string]string) + len := len(context.File.Body().Blocks()) + for i, block := range context.File.Body().Blocks() { + switch block.Type() { + case "terraform", "provider", "variable": + continue + default: + key := fmt.Sprintf("%s.%s", block.Type(), strings.Join(block.Labels(), ".")) + if _, ok := blockMap[key]; ok { + continue + } + outputFilename := "dependency.tf" + if i == len-1 { + outputFilename = "testing.tf" + } + contentToAppend[outputFilename] = contentToAppend[outputFilename] + "\n" + string(block.BuildTokens(nil).Bytes()) + } + } + + for filename, content := range contentToAppend { + err := appendContent(path.Join(wd, filename), content) if err != nil { - log.Fatalf("[ERROR] error loading data source: %+v\n", err) + logrus.Errorf("writing %s: %+v", filename, err) + } + logrus.Infof("configuration is written to %s", path.Join(wd, filename)) + } + return 0 +} + +func (c GenerateCommand) fromSwaggerPath() int { + logrus.Infof("loading swagger spec: %s...", c.swaggerPath) + file, err := os.Stat(c.swaggerPath) + if err != nil { + logrus.Fatalf("loading swagger spec: %+v", err) + } + apiPathsAll := make([]swagger.ApiPath, 0) + if file.IsDir() { + logrus.Infof("swagger spec is a directory") + logrus.Infof("loading swagger spec directory: %s...", c.swaggerPath) + files, err := os.ReadDir(c.swaggerPath) + if err != nil { + logrus.Fatalf("reading swagger spec directory: %+v", err) + } + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".json") || file.IsDir() { + continue + } + filename := path.Join(c.swaggerPath, file.Name()) + logrus.Infof("parsing swagger spec: %s...", filename) + apiPaths, err := swagger.Load(filename) + if err != nil { + logrus.Fatalf("parsing swagger spec: %+v", err) + } + apiPathsAll = append(apiPathsAll, apiPaths...) } } else { - base, err = resource.NewResourceFromExample(exampleFilepath) + logrus.Infof("parsing swagger spec: %s...", c.swaggerPath) + apiPaths, err := swagger.Load(c.swaggerPath) if err != nil { - log.Fatalf("[ERROR] error loading resource: %+v\n", err) + logrus.Fatalf("parsing swagger spec: %+v", err) } + apiPathsAll = append(apiPathsAll, apiPaths...) } - // generate dependency.tf - requiredDependencies := base.RequiredDependencies(existDeps, deps) - dependencyHcl := "" - for _, dep := range requiredDependencies { - dependencyHcl = hcl.Combine(dependencyHcl, hcl.RenameLabel(dep.ExampleConfiguration)) + logrus.Infof("found %d api paths", len(apiPathsAll)) + return c.generate(apiPathsAll) +} + +func (c *GenerateCommand) fromAutorestConfig() int { + logrus.Infof("parsing autorest config: %s...", c.readmePath) + packages := autorest.ParseAutoRestConfig(c.readmePath) + logrus.Debugf("found %d packages", len(packages)) + var targetPackage *autorest.Package + for _, pkg := range packages { + if pkg.Tag == c.tag { + targetPackage = &pkg + break + } } - err = appendFile(path.Join(wd, "dependency.tf"), dependencyHcl) - if err != nil { - log.Fatalf("[ERROR] error writing dependency.tf: %+v\n", err) + if targetPackage == nil { + logrus.Fatalf("package with tag %s not found in %s", c.tag, c.readmePath) } - log.Println("[INFO] dependency.tf generated") - // generate testing.tf - refs := make([]resource.Reference, 0) - for _, dep := range hcl.LoadExistingDependencies(wd) { - refs = append(refs, *resource.NewReferenceFromAddress(fmt.Sprintf("%s.%s", dep.Address, dep.ReferredProperty))) + apiPathsAll := make([]swagger.ApiPath, 0) + for _, swaggerPath := range targetPackage.InputFiles { + logrus.Infof("parsing swagger spec: %s...", swaggerPath) + azapiPaths, err := swagger.Load(swaggerPath) + if err != nil { + logrus.Fatalf("parsing swagger spec: %+v", err) + } + apiPathsAll = append(apiPathsAll, azapiPaths...) } - base.UpdatePropertyDependencyMappingsReference(append(deps, existDeps...), refs) - base.GenerateLabel(refs) - testResourceHcl := base.Hcl(c.useRawJsonPayload) - err = appendFile(path.Join(wd, "testing.tf"), testResourceHcl) - if err != nil { - log.Fatalf("[ERROR] error writing testing.tf: %+v\n", err) + + return c.generate(apiPathsAll) +} + +func (c *GenerateCommand) generate(apiPaths []swagger.ApiPath) int { + wd := c.workingDir + azapiDefinitionsAll := make([]types.AzapiDefinition, 0) + for _, apiPath := range apiPaths { + azapiDefinitionsAll = append(azapiDefinitionsAll, resource.NewAzapiDefinitionsFromSwagger(apiPath)...) + } + + for i := range azapiDefinitionsAll { + if c.useRawJsonPayload { + azapiDefinitionsAll[i].BodyFormat = types.BodyFormatJson + } + } + + azapiDefinitionByResourceType := make(map[string][]types.AzapiDefinition) + for _, azapiDefinition := range azapiDefinitionsAll { + azapiDefinitionByResourceType[azapiDefinition.AzureResourceType] = append(azapiDefinitionByResourceType[azapiDefinition.AzureResourceType], azapiDefinition) + } + + resourceTypes := make([]string, 0) + for resourceType, _ := range azapiDefinitionByResourceType { + slices.SortFunc(azapiDefinitionByResourceType[resourceType], func(i, j types.AzapiDefinition) int { + return azapiDefinitionOrder(i) - azapiDefinitionOrder(j) + }) + resourceTypes = append(resourceTypes, resourceType) + } + + sort.Strings(resourceTypes) + + referenceResolvers := []resolver.ReferenceResolver{ + resolver.NewAzapiDependencyResolver(), + resolver.NewAzapiDefinitionResolver(azapiDefinitionsAll), + resolver.NewProviderIDResolver(), + resolver.NewLocationIDResolver(), + resolver.NewAzapiResourceIdResolver(), + } + + for _, resourceType := range resourceTypes { + logrus.Infof("generating terraform configurations for %s...", resourceType) + azapiDefinitions := azapiDefinitionByResourceType[resourceType] + // remove existing folders by default + folderName := strings.ReplaceAll(resourceType, "/", "_") + err := os.RemoveAll(path.Join(wd, folderName)) + if err != nil { + logrus.Errorf("removing existing folder: %+v", err) + } + err = os.MkdirAll(path.Join(wd, folderName), 0755) + if err != nil { + logrus.Fatalf("creating folder: %+v", err) + } + + context := resource.NewContext(referenceResolvers) + + for _, azapiDefinition := range azapiDefinitions { + logrus.Debugf("generating terraform configurations for %s...", azapiDefinition.Id) + err = context.AddAzapiDefinition(azapiDefinition) + if err != nil { + logrus.Warnf("adding azapi definition for %s: %+v", azapiDefinition.Id, err) + } + } + + filename := path.Join(wd, folderName, "main.tf") + err = os.WriteFile(filename, hclwrite.Format([]byte(context.String())), 0644) + if err != nil { + logrus.Errorf("writing %s: %+v", filename, err) + } + + // TODO: remove the following code + if err := postCheck(path.Join(wd, folderName)); err != nil { + logrus.Errorf("post check: %+v", err) + } + os.RemoveAll(path.Join(wd, folderName, "log.txt")) + os.RemoveAll(path.Join(wd, folderName, "tfplan")) } - log.Println("[INFO] testing.tf generated") return 0 } -func appendFile(filename string, hclContent string) error { +func postCheck(workingDirectory string) error { + logrus.Infof("post check for %s...", workingDirectory) + t, err := tf.NewTerraform(workingDirectory, false) + if err != nil { + return err + } + validateOutput, err := t.Validate() + if err != nil { + return err + } + if !validateOutput.Valid { + return fmt.Errorf("invalid terraform configuration: %+v", validateOutput) + } + _, err = t.Plan() + if err != nil { + return err + } + return nil +} + +func azapiDefinitionOrder(azapiDefinition types.AzapiDefinition) int { + switch azapiDefinition.ResourceName { + case "azapi_resource": + return 0 + case "azapi_update_resource": + return 1 + case "azapi_resource_action": + return 2 + case "azapi_resource_list": + return 3 + } + return 4 +} + +func appendContent(filename string, hclContent string) error { content := hclContent if _, err := os.Stat(filename); err == nil { existingHcl, err := os.ReadFile(filename) if err != nil { - log.Printf("[WARN] reading %s: %+v", filename, err) - } else { - content = hcl.Combine(string(existingHcl), content) + logrus.Warnf("reading existing file: %+v", err) } + content = string(existingHcl) + "\n" + content } return os.WriteFile(filename, hclwrite.Format([]byte(content)), 0644) } -func loadDependencies(workingDir string) ([]types.Dependency, []types.Dependency) { - mappingJsonLoader := loader.MappingJsonDependencyLoader{} - hardcodeLoader := loader.HardcodeDependencyLoader{} - - deps := make([]types.Dependency, 0) - depsMap := make(map[string]types.Dependency) - if temp, err := mappingJsonLoader.Load(); err == nil { - for _, dep := range temp { - depsMap[dep.ResourceType+"."+dep.ReferredProperty] = dep - } +func blockFileMap(workingDirectory string) map[string]string { + files, err := os.ReadDir(workingDirectory) + if err != nil { + logrus.Warnf("reading dir %s: %+v", workingDirectory, err) + return nil } - if temp, err := hardcodeLoader.Load(); err == nil { - for _, dep := range temp { - depsMap[dep.ResourceType+"."+dep.ReferredProperty] = dep + out := make(map[string]string) + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".tf") { + continue + } + src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) + if err != nil { + logrus.Warnf("reading file %s: %+v", file.Name(), err) + continue + } + f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos) + if diag.HasErrors() { + logrus.Warnf("parsing file %s: %+v", file.Name(), diag.Error()) + continue + } + if f == nil || f.Body() == nil { + continue + } + for _, block := range f.Body().Blocks() { + key := fmt.Sprintf("%s.%s", block.Type(), strings.Join(block.Labels(), ".")) + out[key] = file.Name() } } - for _, dep := range depsMap { - deps = append(deps, dep) + + return out +} + +func allTerraformConfig(workingDirectory string) string { + out := "" + files, err := os.ReadDir(workingDirectory) + if err != nil { + logrus.Warnf("reading dir %s: %+v", workingDirectory, err) + return out } - existDeps := hcl.LoadExistingDependencies(workingDir) - for i := range existDeps { - ref := existDeps[i].ResourceType + "." + existDeps[i].ReferredProperty - if dep, ok := depsMap[ref]; ok { - existDeps[i].Pattern = dep.Pattern + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".tf") { + continue } + src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) + if err != nil { + logrus.Warnf("reading file %s: %+v", file.Name(), err) + continue + } + out += string(src) } - return existDeps, deps + + return out } diff --git a/commands/generate_test.go b/commands/generate_test.go index ee39ded2..f2b3102a 100644 --- a/commands/generate_test.go +++ b/commands/generate_test.go @@ -5,7 +5,6 @@ import ( "path" "testing" - "github.com/mitchellh/cli" "github.com/ms-henglu/armstrong/commands" "github.com/ms-henglu/armstrong/tf" ) @@ -43,16 +42,7 @@ func runGenerateCommand(args [][]string, t *testing.T) { } defer os.RemoveAll(wd) - ui := &cli.ColoredUi{ - ErrorColor: cli.UiColorRed, - WarnColor: cli.UiColorYellow, - Ui: &cli.BasicUi{ - Writer: os.Stdout, - Reader: os.Stdin, - ErrorWriter: os.Stderr, - }, - } - command := commands.GenerateCommand{Ui: ui} + command := commands.GenerateCommand{} for _, arg := range args { res := command.Run(append([]string{"-working-dir", wd}, arg...)) diff --git a/commands/setup.go b/commands/setup.go deleted file mode 100644 index 5157989a..00000000 --- a/commands/setup.go +++ /dev/null @@ -1,77 +0,0 @@ -package commands - -import ( - "flag" - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/mitchellh/cli" - "github.com/ms-henglu/armstrong/tf" -) - -type SetupCommand struct { - Ui cli.Ui - verbose bool - workingDir string -} - -func (c *SetupCommand) flags() *flag.FlagSet { - fs := defaultFlagSet("setup") - fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") - fs.StringVar(&c.workingDir, "working-dir", "", "path to Terraform configuration files") - fs.Usage = func() { c.Ui.Error(c.Help()) } - return fs -} - -func (c SetupCommand) Help() string { - helpText := ` -Usage: armstrong setup [-v] [-working-dir ] -` + c.Synopsis() + "\n\n" + helpForFlags(c.flags()) - - return strings.TrimSpace(helpText) -} - -func (c SetupCommand) Synopsis() string { - return "Update dependencies for tests" -} - -func (c SetupCommand) Run(args []string) int { - f := c.flags() - if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) - return 1 - } - return c.Execute() -} - -func (c SetupCommand) Execute() int { - log.Println("[INFO] ----------- update resources ---------") - wd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("failed to get working directory: %+v", err)) - return 1 - } - if c.workingDir != "" { - wd, err = filepath.Abs(c.workingDir) - if err != nil { - c.Ui.Error(fmt.Sprintf("working directory is invalid: %+v", err)) - return 1 - } - } - terraform, err := tf.NewTerraform(wd, c.verbose) - if err != nil { - log.Fatalf("[ERROR] error creating terraform executable: %+v\n", err) - } - log.Printf("[INFO] prepare working directory\n") - _ = terraform.Init() - log.Println("[INFO] running apply command to update dependency resources...") - err = terraform.Apply() - if err != nil { - log.Fatalf("[ERROR] error setting up resources: %+v\n", err) - } - log.Println("[INFO] all dependencies have been updated") - return 0 -} diff --git a/commands/test.go b/commands/test.go index 57058aab..44b84b3c 100644 --- a/commands/test.go +++ b/commands/test.go @@ -10,15 +10,14 @@ import ( "strings" "time" - "github.com/mitchellh/cli" "github.com/ms-henglu/armstrong/coverage" "github.com/ms-henglu/armstrong/report" "github.com/ms-henglu/armstrong/tf" "github.com/ms-henglu/armstrong/types" + "github.com/sirupsen/logrus" ) type TestCommand struct { - Ui cli.Ui verbose bool workingDir string } @@ -27,7 +26,7 @@ func (c *TestCommand) flags() *flag.FlagSet { fs := defaultFlagSet("test") fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") fs.StringVar(&c.workingDir, "working-dir", "", "path to Terraform configuration files") - fs.Usage = func() { c.Ui.Error(c.Help()) } + fs.Usage = func() { logrus.Error(c.Help()) } return fs } @@ -46,9 +45,12 @@ func (c TestCommand) Synopsis() string { func (c TestCommand) Run(args []string) int { f := c.flags() if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) + logrus.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) return 1 } + if c.verbose { + logrus.SetLevel(logrus.DebugLevel) + } return c.Execute() } @@ -61,13 +63,13 @@ func (c TestCommand) Execute() int { log.Println("[INFO] ----------- run tests ---------") wd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("failed to get working directory: %+v", err)) + logrus.Error(fmt.Sprintf("failed to get working directory: %+v", err)) return 1 } if c.workingDir != "" { wd, err = filepath.Abs(c.workingDir) if err != nil { - c.Ui.Error(fmt.Sprintf("working directory is invalid: %+v", err)) + logrus.Error(fmt.Sprintf("working directory is invalid: %+v", err)) return 1 } } diff --git a/commands/validate.go b/commands/validate.go index a83af3f5..ff65b7c7 100644 --- a/commands/validate.go +++ b/commands/validate.go @@ -3,24 +3,24 @@ package commands import ( "flag" "fmt" - "log" "os" "path/filepath" "strings" - "github.com/mitchellh/cli" "github.com/ms-henglu/armstrong/tf" + "github.com/sirupsen/logrus" ) type ValidateCommand struct { - Ui cli.Ui + verbose bool workingDir string } func (c *ValidateCommand) flags() *flag.FlagSet { fs := defaultFlagSet("validate") fs.StringVar(&c.workingDir, "working-dir", "", "path to Terraform configuration files") - fs.Usage = func() { c.Ui.Error(c.Help()) } + fs.BoolVar(&c.verbose, "v", false, "whether show terraform logs") + fs.Usage = func() { logrus.Error(c.Help()) } return fs } @@ -39,37 +39,40 @@ func (c ValidateCommand) Synopsis() string { func (c ValidateCommand) Run(args []string) int { f := c.flags() if err := f.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) + logrus.Error(fmt.Sprintf("Error parsing command-line flags: %s", err)) return 1 } + if c.verbose { + logrus.SetLevel(logrus.DebugLevel) + } return c.Execute() } func (c ValidateCommand) Execute() int { wd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("failed to get working directory: %+v", err)) + logrus.Error(fmt.Sprintf("failed to get working directory: %+v", err)) return 1 } if c.workingDir != "" { wd, err = filepath.Abs(c.workingDir) if err != nil { - c.Ui.Error(fmt.Sprintf("working directory is invalid: %+v", err)) + logrus.Error(fmt.Sprintf("working directory is invalid: %+v", err)) return 1 } } terraform, err := tf.NewTerraform(wd, true) if err != nil { - log.Fatalf("[ERROR] error creating terraform executable: %+v\n", err) + logrus.Fatalf("creating terraform executable: %+v\n", err) } - log.Printf("[INFO] prepare working directory\n") + logrus.Infof("running terraform init...") _ = terraform.Init() - log.Println("[INFO] running plan command to check changes...") + logrus.Infof("running terraform plan to check the changes...") plan, err := terraform.Plan() if err != nil { - log.Fatalf("[ERROR] error running terraform plan: %+v\n", err) + logrus.Fatalf("running terraform plan: %+v", err) } _ = tf.GetChanges(plan) diff --git a/dependency/azapi.go b/dependency/azapi.go new file mode 100644 index 00000000..8a37f8fe --- /dev/null +++ b/dependency/azapi.go @@ -0,0 +1,89 @@ +package dependency + +import ( + "embed" + "fmt" + "path" + "strings" + "sync" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" +) + +type Dependency struct { + AzureResourceType string + ApiVersion string + ExampleConfiguration string + ResourceKind string // resource or data + ReferredProperty string // only supports "id" for now + ResourceName string + ResourceLabel string +} + +//go:embed azapi_examples +var StaticFiles embed.FS + +var azapiMutex = sync.Mutex{} + +var azapiDeps = make([]Dependency, 0) + +func LoadAzapiDependencies() ([]Dependency, error) { + azapiMutex.Lock() + defer azapiMutex.Unlock() + if len(azapiDeps) != 0 { + return azapiDeps, nil + } + + dir := "azapi_examples" + entries, err := StaticFiles.ReadDir(dir) + if err != nil { + return nil, err + } + for _, entry := range entries { + filename := path.Join(dir, entry.Name(), "main.tf") + data, err := StaticFiles.ReadFile(filename) + if err != nil { + return nil, err + } + f, diags := hclwrite.ParseConfig(data, filename, hcl.InitialPos) + if diags.HasErrors() { + return nil, diags + } + blockTotal := len(f.Body().Blocks()) + lastBlock := f.Body().Blocks()[blockTotal-1] + typeValue := string(lastBlock.Body().GetAttribute("type").Expr().BuildTokens(nil).Bytes()) + typeValue = strings.Trim(typeValue, ` "`) + parts := strings.Split(typeValue, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("resource type is invalid: %s, filename: %s", typeValue, filename) + } + dep := Dependency{ + AzureResourceType: parts[0], + ApiVersion: parts[1], + ExampleConfiguration: string(data), + ReferredProperty: "id", + ResourceKind: lastBlock.Type(), + ResourceName: lastBlock.Labels()[0], + ResourceLabel: lastBlock.Labels()[1], + } + azapiDeps = append(azapiDeps, dep) + } + + // add a special case for Microsoft.Resources/subscriptions + azapiDeps = append(azapiDeps, Dependency{ + AzureResourceType: "Microsoft.Resources/subscriptions", + ApiVersion: "2020-06-01", + ReferredProperty: "id", + ResourceKind: "data", + ResourceName: "azapi_resource", + ResourceLabel: "subscription", + ExampleConfiguration: ` +data "azapi_resource" "subscription" { + type = "Microsoft.Resources/subscriptions@2020-06-01" + response_export_values = ["*"] +} +`, + }) + return azapiDeps, nil +} diff --git a/dependency/azapi_test.go b/dependency/azapi_test.go new file mode 100644 index 00000000..9517c28a --- /dev/null +++ b/dependency/azapi_test.go @@ -0,0 +1,39 @@ +package dependency_test + +import ( + "testing" + + "github.com/ms-henglu/armstrong/dependency" +) + +func Test_LoadAzapiDependencies(t *testing.T) { + res, err := dependency.LoadAzapiDependencies() + if err != nil { + t.Error(err) + } + if len(res) == 0 { + t.Error("No dependencies loaded") + } + + for _, dep := range res { + if dep.AzureResourceType == "" { + t.Errorf("AzureResourceType is empty for %s", dep.ResourceName) + } + if dep.ApiVersion == "" { + t.Errorf("ApiVersion is empty for %s", dep.ResourceName) + } + if dep.ExampleConfiguration == "" { + t.Errorf("ExampleConfiguration is empty for %s", dep.ResourceName) + } + if dep.ResourceKind == "" { + t.Errorf("ResourceKind is empty for %s", dep.ResourceName) + } + if dep.ResourceName == "" { + t.Errorf("ResourceName is empty for %s", dep.ResourceName) + } + if dep.ResourceLabel == "" { + t.Errorf("ResourceLabel is empty for %s", dep.ResourceName) + } + + } +} diff --git a/dependency/azurerm.go b/dependency/azurerm.go new file mode 100644 index 00000000..66291c91 --- /dev/null +++ b/dependency/azurerm.go @@ -0,0 +1,59 @@ +package dependency + +import ( + "strings" + "sync" + + "github.com/ms-henglu/armstrong/dependency/azurerm" +) + +var azurermMutex = sync.Mutex{} + +var azurermDeps = make([]Dependency, 0) + +func LoadAzurermDependencies() []Dependency { + azurermMutex.Lock() + defer azurermMutex.Unlock() + if len(azurermDeps) != 0 { + return azurermDeps + } + + mappingJsonLoader := azurerm.MappingJsonDependencyLoader{} + hardcodeLoader := azurerm.HardcodeDependencyLoader{} + + depsMap := make(map[string]azurerm.Mapping) + if temp, err := mappingJsonLoader.Load(); err == nil { + for _, dep := range temp { + depsMap[dep.ResourceType] = dep + } + } + if temp, err := hardcodeLoader.Load(); err == nil { + for _, dep := range temp { + depsMap[dep.ResourceType] = dep + } + } + + deps := make([]azurerm.Mapping, 0) + for _, dep := range depsMap { + deps = append(deps, dep) + } + + azurermDeps = make([]Dependency, 0) + for _, dep := range deps { + startStr := "providers/" + index := strings.LastIndex(dep.IdPattern, startStr) + if index == -1 { + continue + } + azureResourceType := dep.IdPattern[index+len(startStr):] + azurermDeps = append(azurermDeps, Dependency{ + AzureResourceType: azureResourceType, + ExampleConfiguration: dep.ExampleConfiguration, + ResourceKind: "resource", + ReferredProperty: "id", + ResourceName: dep.ResourceType, + ResourceLabel: "", + }) + } + return azurermDeps +} diff --git a/dependency/azurerm/loader.go b/dependency/azurerm/loader.go index bdee2de9..f7c50e66 100644 --- a/dependency/azurerm/loader.go +++ b/dependency/azurerm/loader.go @@ -1,7 +1,11 @@ -package loader +package azurerm -import "github.com/ms-henglu/armstrong/types" +type Mapping struct { + ResourceType string `json:"resourceType"` + ExampleConfiguration string `json:"exampleConfiguration,omitempty"` + IdPattern string `json:"idPattern"` +} type DependencyLoader interface { - Load() ([]types.Dependency, error) + Load() ([]Mapping, error) } diff --git a/dependency/azurerm/loader_hardcode.go b/dependency/azurerm/loader_hardcode.go index 7adb120b..e203a029 100644 --- a/dependency/azurerm/loader_hardcode.go +++ b/dependency/azurerm/loader_hardcode.go @@ -1,14 +1,12 @@ -package loader - -import "github.com/ms-henglu/armstrong/types" +package azurerm type HardcodeDependencyLoader struct { } -func (h HardcodeDependencyLoader) Load() ([]types.Dependency, error) { - return []types.Dependency{ +func (h HardcodeDependencyLoader) Load() ([]Mapping, error) { + return []Mapping{ { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.Network/virtualNetworks/subnets", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.Network/virtualNetworks/subnets", ExampleConfiguration: ` provider "azurerm" { features {} @@ -33,11 +31,10 @@ resource "azurerm_subnet" "test" { address_prefixes = ["10.0.1.0/24"] } `, - ResourceType: "azurerm_subnet", - ReferredProperty: "id", + ResourceType: "azurerm_subnet", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.Sql/servers/databases", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.Sql/servers/databases", ExampleConfiguration: ` provider "azurerm" { features {} @@ -70,11 +67,10 @@ resource "azurerm_mssql_database" "test" { server_id = azurerm_mssql_server.test.id } `, - ResourceType: "azurerm_mssql_database", - ReferredProperty: "id", + ResourceType: "azurerm_mssql_database", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforMariaDB/servers", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforMariaDB/servers", ExampleConfiguration: ` provider "azurerm" { features {} @@ -104,11 +100,10 @@ resource "azurerm_mariadb_server" "example" { ssl_enforcement_enabled = true } `, - ResourceType: "azurerm_mariadb_server", - ReferredProperty: "id", + ResourceType: "azurerm_mariadb_server", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforMySQL/flexibleServers", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforMySQL/flexibleServers", ExampleConfiguration: ` provider "azurerm" { features {} @@ -129,11 +124,10 @@ resource "azurerm_mysql_flexible_server" "example" { sku_name = "GP_Standard_D2ds_v4" } `, - ResourceType: "azurerm_mysql_flexible_server", - ReferredProperty: "id", + ResourceType: "azurerm_mysql_flexible_server", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforPostgreSQL/flexibleServers", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.DBforPostgreSQL/flexibleServers", ExampleConfiguration: ` provider "azurerm" { features {} @@ -158,11 +152,10 @@ resource "azurerm_postgresql_flexible_server" "example" { } `, - ResourceType: "azurerm_postgresql_flexible_server", - ReferredProperty: "id", + ResourceType: "azurerm_postgresql_flexible_server", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.Sql/servers", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.Sql/servers", ExampleConfiguration: ` resource "azurerm_resource_group" "example" { name = "database-rg" @@ -198,11 +191,10 @@ resource "azurerm_mssql_server" "example" { } } `, - ResourceType: "azurerm_mssql_server", - ReferredProperty: "id", + ResourceType: "azurerm_mssql_server", }, { - Pattern: "/subscriptions/resourceGroups/providers/Microsoft.Synapse/workspaces", + IdPattern: "/subscriptions/resourceGroups/providers/Microsoft.Synapse/workspaces", ExampleConfiguration: ` resource "azurerm_resource_group" "example" { name = "example-resources" @@ -237,125 +229,105 @@ resource "azurerm_synapse_workspace" "example" { } } `, - ResourceType: "azurerm_synapse_workspace", - ReferredProperty: "id", + ResourceType: "azurerm_synapse_workspace", }, // override those customer managed key resource which is not a real resource { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_log_analytics_cluster_customer_managed_key", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_eventhub_namespace_customer_managed_key", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_cognitive_account_customer_managed_key", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_storage_account_customer_managed_key", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_kusto_cluster_customer_managed_key", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_databricks_workspace_customer_managed_key", - ReferredProperty: "id", }, // override role assignment { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_synapse_role_assignment", - ReferredProperty: "id", }, // override all kinds of associations { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_network_interface_nat_rule_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_subnet_route_table_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_network_interface_application_security_group_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_virtual_desktop_workspace_application_group_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_nat_gateway_public_ip_prefix_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_management_group_subscription_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_network_interface_application_gateway_backend_address_pool_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_subnet_nat_gateway_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_subnet_network_security_group_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_network_interface_backend_address_pool_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_network_interface_security_group_association", - ReferredProperty: "id", }, { - Pattern: "", + IdPattern: "", ExampleConfiguration: "", ResourceType: "azurerm_nat_gateway_public_ip_association", - ReferredProperty: "id", }, }, nil } diff --git a/dependency/azurerm/loader_mapping_json.go b/dependency/azurerm/loader_mapping_json.go index 38421512..a00d6cc1 100644 --- a/dependency/azurerm/loader_mapping_json.go +++ b/dependency/azurerm/loader_mapping_json.go @@ -1,11 +1,9 @@ -package loader +package azurerm import ( _ "embed" "encoding/json" "os" - - "github.com/ms-henglu/armstrong/types" ) type MappingJsonDependencyLoader struct { @@ -15,33 +13,23 @@ type MappingJsonDependencyLoader struct { //go:embed mappings.json var mappingsJson string -func (m MappingJsonDependencyLoader) Load() ([]types.Dependency, error) { - var mappings []types.Mapping +func (m MappingJsonDependencyLoader) Load() ([]Mapping, error) { + var mappings []Mapping var data []byte var err error if len(m.MappingJsonFilepath) > 0 { data, err = os.ReadFile(m.MappingJsonFilepath) if err != nil { - return []types.Dependency{}, err + return nil, err } } else { data = []byte(mappingsJson) } err = json.Unmarshal(data, &mappings) if err != nil { - return []types.Dependency{}, err - } - deps := make([]types.Dependency, 0) - for _, mapping := range mappings { - deps = append(deps, types.Dependency{ - Pattern: mapping.IdPattern, - ExampleConfiguration: mapping.ExampleConfiguration, - ResourceType: mapping.ResourceType, - ReferredProperty: "id", - }) + return nil, err } - - return deps, nil + return mappings, nil } var _ DependencyLoader = MappingJsonDependencyLoader{} diff --git a/dependency/azurerm_test.go b/dependency/azurerm_test.go new file mode 100644 index 00000000..b916544e --- /dev/null +++ b/dependency/azurerm_test.go @@ -0,0 +1,28 @@ +package dependency_test + +import ( + "testing" + + "github.com/ms-henglu/armstrong/dependency" +) + +func Test_LoadAzurermDependencies(t *testing.T) { + res := dependency.LoadAzurermDependencies() + if len(res) == 0 { + t.Error("No dependencies loaded") + } + for _, dep := range res { + if dep.AzureResourceType == "" { + t.Errorf("AzureResourceType is empty for %s", dep.ResourceName) + } + if dep.ExampleConfiguration == "" { + t.Errorf("ExampleConfiguration is empty for %s", dep.ResourceName) + } + if dep.ResourceKind == "" { + t.Errorf("ResourceKind is empty for %s", dep.ResourceName) + } + if dep.ResourceName == "" { + t.Errorf("ResourceName is empty for %s", dep.ResourceName) + } + } +} diff --git a/dependency/pattern.go b/dependency/pattern.go new file mode 100644 index 00000000..ae6018e4 --- /dev/null +++ b/dependency/pattern.go @@ -0,0 +1,62 @@ +package dependency + +import ( + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/ms-henglu/armstrong/utils" +) + +type Pattern struct { + AzureResourceType string + Scope Scope + Placeholder string +} + +type Scope string + +const ( + ScopeTenant Scope = "tenant" + ScopeSubscription Scope = "subscription" + ScopeResourceGroup Scope = "resource_group" + ScopeResource Scope = "resource" +) + +func (p Pattern) String() string { + return strings.ToLower(string(p.Scope) + ":" + p.AzureResourceType) +} + +func (p Pattern) IsMatch(input string) bool { + resourceType := utils.ResourceTypeOfResourceId(input) + if resourceType != p.AzureResourceType { + return false + } + parentId := utils.ParentIdOfResourceId(input) + scope := scopeOfResourceId(parentId) + return scope == p.Scope +} + +func NewPattern(idPlaceholder string) Pattern { + resourceType := utils.ResourceTypeOfResourceId(idPlaceholder) + parentId := utils.ParentIdOfResourceId(idPlaceholder) + scope := scopeOfResourceId(parentId) + return Pattern{ + AzureResourceType: resourceType, + Scope: scope, + Placeholder: idPlaceholder, + } +} + +func scopeOfResourceId(input string) Scope { + resourceType := utils.ResourceTypeOfResourceId(input) + switch resourceType { + case arm.TenantResourceType.String(): + return ScopeTenant + case arm.SubscriptionResourceType.String(): + return ScopeSubscription + case arm.ResourceGroupResourceType.String(): + return ScopeResourceGroup + default: + return ScopeResource + } +} diff --git a/dependency/pattern_test.go b/dependency/pattern_test.go new file mode 100644 index 00000000..d4d03834 --- /dev/null +++ b/dependency/pattern_test.go @@ -0,0 +1,147 @@ +package dependency_test + +import ( + "log" + "testing" + + "github.com/ms-henglu/armstrong/dependency" +) + +func Test_NewPattern(t *testing.T) { + testcases := []struct { + input string + expected dependency.Pattern + }{ + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/test", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network/virtualNetworks/subnets", + Scope: dependency.ScopeResource, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network/virtualNetworks", + Scope: dependency.ScopeResourceGroup, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Resources/resourceGroups", + Scope: dependency.ScopeSubscription, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Resources/subscriptions", + Scope: dependency.ScopeTenant, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/test/providers/Microsoft.Network/networkSecurityGroups/test", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network/networkSecurityGroups", + Scope: dependency.ScopeResource, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network", + Scope: dependency.ScopeResourceGroup, + }, + }, + { + input: "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Network", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network", + Scope: dependency.ScopeSubscription, + }, + }, + { + input: "/providers/Microsoft.Network", + expected: dependency.Pattern{ + AzureResourceType: "Microsoft.Network", + Scope: dependency.ScopeTenant, + }, + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] input: %s", testcase.input) + actual := dependency.NewPattern(testcase.input) + if actual.AzureResourceType != testcase.expected.AzureResourceType { + t.Errorf("expected %s, got %s", testcase.expected.AzureResourceType, actual.AzureResourceType) + } + if actual.Scope != testcase.expected.Scope { + t.Errorf("expected %s, got %s", testcase.expected.Scope, actual.Scope) + } + } +} + +func Test_IsMatch(t *testing.T) { + testcases := []struct { + Input string + Pattern dependency.Pattern + Match bool + }{ + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/test", + Pattern: dependency.NewPattern("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/test"), + Match: true, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test", + Pattern: dependency.NewPattern("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test"), + Match: true, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test", + Pattern: dependency.NewPattern("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subntes/test"), + Match: false, + }, + } + + for _, testcase := range testcases { + t.Logf("input: %s", testcase.Input) + actual := testcase.Pattern.IsMatch(testcase.Input) + if actual != testcase.Match { + t.Errorf("expected %v, got %v", testcase.Match, actual) + } + } +} + +func Test_String(t *testing.T) { + testcases := []struct { + Input string + Expect string + }{ + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test/subnets/test", + Expect: "resource:microsoft.network/virtualnetworks/subnets", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test/providers/Microsoft.Network/virtualNetworks/test", + Expect: "resource_group:microsoft.network/virtualnetworks", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test", + Expect: "subscription:microsoft.resources/resourcegroups", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expect: "tenant:microsoft.resources/subscriptions", + }, + } + + for _, testcase := range testcases { + t.Logf("input: %s", testcase.Input) + actual := dependency.NewPattern(testcase.Input).String() + if actual != testcase.Expect { + t.Errorf("expected %s, got %s", testcase.Expect, actual) + } + } +} diff --git a/hcl/marshal.go b/hcl/marshal.go index 9c826925..7d25674e 100644 --- a/hcl/marshal.go +++ b/hcl/marshal.go @@ -21,10 +21,10 @@ func MarshalIndent(input interface{}, prefix, indent string) string { for _, key := range keys { value := i[key] wrapKey := key - if strings.Contains(key, "/") || startsWithNumber(key) { - wrapKey = fmt.Sprintf(`"%s"`, key) - } else if strings.HasPrefix(key, "${") && strings.HasSuffix(key, "}") { + if strings.HasPrefix(key, "${") && strings.HasSuffix(key, "}") { wrapKey = fmt.Sprintf("(%s)", key[2:len(key)-1]) + } else if needWrapKey(key) { + wrapKey = fmt.Sprintf(`"%s"`, key) } content += fmt.Sprintf("%s%s = %s\n", prefix+indent, wrapKey, MarshalIndent(value, prefix+indent, indent)) } @@ -39,15 +39,26 @@ func MarshalIndent(input interface{}, prefix, indent string) string { if strings.HasPrefix(i, "${") && strings.HasSuffix(i, "}") { return i[2 : len(i)-1] } + i = strings.ReplaceAll(i, "\\", "\\\\") // escape backslashes + i = strings.ReplaceAll(i, "\n", "\\n") + i = strings.ReplaceAll(i, "\t", "\\t") + i = strings.ReplaceAll(i, "\r", "\\r") + i = strings.ReplaceAll(i, "\"", "\\\"") return fmt.Sprintf(`"%s"`, i) default: return fmt.Sprintf("%v", i) } } -func startsWithNumber(input string) bool { +func needWrapKey(input string) bool { if len(input) == 0 { return false } + if strings.Contains(input, ".") || strings.Contains(input, "/") { + return true + } + if input[0] == '$' { + return true + } return input[0] >= '0' && input[0] <= '9' } diff --git a/main.go b/main.go index 8ac25edc..8c09240b 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,12 @@ import ( "github.com/mitchellh/cli" "github.com/ms-henglu/armstrong/commands" + "github.com/sirupsen/logrus" ) func main() { + logrus.SetLevel(logrus.InfoLevel) + c := &cli.CLI{ Name: "armstrong", Version: VersionString(), @@ -15,52 +18,24 @@ func main() { HelpWriter: os.Stdout, } - ui := &cli.ColoredUi{ - ErrorColor: cli.UiColorRed, - WarnColor: cli.UiColorYellow, - Ui: &cli.BasicUi{ - Writer: os.Stdout, - Reader: os.Stdin, - ErrorWriter: os.Stderr, - }, - } - c.Commands = map[string]cli.CommandFactory{ - "auto": func() (cli.Command, error) { - return &commands.AutoCommand{ - Ui: ui, - }, nil - }, "generate": func() (cli.Command, error) { - return &commands.GenerateCommand{ - Ui: ui, - }, nil + return &commands.GenerateCommand{}, nil }, - "cleanup": func() (cli.Command, error) { - return &commands.CleanupCommand{ - Ui: ui, - }, nil - }, - "setup": func() (cli.Command, error) { - return &commands.SetupCommand{ - Ui: ui, - }, nil + "validate": func() (cli.Command, error) { + return &commands.ValidateCommand{}, nil }, "test": func() (cli.Command, error) { - return &commands.TestCommand{ - Ui: ui, - }, nil + return &commands.TestCommand{}, nil }, - "validate": func() (cli.Command, error) { - return &commands.ValidateCommand{ - Ui: ui, - }, nil + "cleanup": func() (cli.Command, error) { + return &commands.CleanupCommand{}, nil }, } exitStatus, err := c.Run() if err != nil { - ui.Error("Error: " + err.Error()) + logrus.Fatal(err) } os.Exit(exitStatus) diff --git a/resource/context.go b/resource/context.go index 16511883..9864fdce 100644 --- a/resource/context.go +++ b/resource/context.go @@ -1,4 +1,4 @@ -package res +package resource import ( "fmt" @@ -8,6 +8,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/resolver" + "github.com/ms-henglu/armstrong/resource/types" "github.com/ms-henglu/armstrong/utils" "github.com/sirupsen/logrus" "github.com/zclconf/go-cty/cty" @@ -17,8 +19,8 @@ type Context struct { File *hclwrite.File locationVarBlock *hclwrite.Block terraformBlock *hclwrite.Block - KnownPatternMap map[string]Reference - ReferenceResolvers []ReferenceResolver + KnownPatternMap map[string]types.Reference + ReferenceResolvers []resolver.ReferenceResolver azapiAddingMap map[string]bool } @@ -45,9 +47,9 @@ variable "location" { } ` -func NewContext(referenceResolvers []ReferenceResolver) *Context { - knownPatternMap := make(map[string]Reference) - referenceResolvers = append([]ReferenceResolver{NewKnownReferenceResolver(knownPatternMap)}, referenceResolvers...) +func NewContext(referenceResolvers []resolver.ReferenceResolver) *Context { + knownPatternMap := make(map[string]types.Reference) + referenceResolvers = append([]resolver.ReferenceResolver{resolver.NewKnownReferenceResolver(knownPatternMap)}, referenceResolvers...) c := Context{ KnownPatternMap: knownPatternMap, ReferenceResolvers: referenceResolvers, @@ -60,6 +62,7 @@ func NewContext(referenceResolvers []ReferenceResolver) *Context { func (c *Context) InitFile(content string) error { file, diags := hclwrite.ParseConfig([]byte(content), "", hcl.InitialPos) if diags.HasErrors() { + logrus.Errorf("failed to parse input:\n%v", content) return diags } var locationVarBlock, terraformBlock *hclwrite.Block @@ -73,13 +76,20 @@ func (c *Context) InitFile(content string) error { terraformBlock = block } } + if terraformBlock == nil { + logrus.Warnf("terraform block not found in the input.") + } + if locationVarBlock == nil { + logrus.Warnf("location variable block not found in the input.") + } c.File = file c.terraformBlock = terraformBlock c.locationVarBlock = locationVarBlock return nil } -func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { +func (c *Context) AddAzapiDefinition(input types.AzapiDefinition) error { + logrus.Debugf("adding azapi definition: \n%v", input) if c.azapiAddingMap[input.Identifier()] { return fmt.Errorf("azapi definition already added: %v", input.Identifier()) } @@ -89,12 +99,12 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { }() def := input.DeepCopy() // find all id placeholders from def - placeHolders := make([]PropertyDependencyMapping, 0) + placeHolders := make([]types.PropertyDependencyMapping, 0) rootFields := []string{"parent_id", "resource_id"} for _, field := range rootFields { if value, ok := def.AdditionalFields[field]; ok { - if literalValue, ok := value.(StringLiteralValue); ok { - placeHolders = append(placeHolders, PropertyDependencyMapping{ + if literalValue, ok := value.(types.StringLiteralValue); ok { + placeHolders = append(placeHolders, types.PropertyDependencyMapping{ ValuePath: field, LiteralValue: literalValue.Literal, }) @@ -102,7 +112,6 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { } } if def.Body != nil { - // TODO: only add resource ID/UUID to the mappings? mappings := GetKeyValueMappings(def.Body, "") for _, mapping := range mappings { if utils.IsResourceId(mapping.LiteralValue) { @@ -110,10 +119,13 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { } } } + logrus.Debugf("found %d id placeholders", len(placeHolders)) // find all dependencies that match the id placeholders for i, placeHolder := range placeHolders { + logrus.Debugf("processing id placeholder: %s", placeHolder.LiteralValue) if utils.IsAction(placeHolder.LiteralValue) { + logrus.Debugf("skip action: %s", placeHolder.LiteralValue) continue } @@ -127,17 +139,22 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { if result == nil { continue } + logrus.Debugf("found dependency by resolver: %T", resolver) switch { case result.Reference.IsKnown(): placeHolders[i].Reference = result.Reference + logrus.Debugf("dependency resolved, ref: %v", result.Reference) case result.HclToAdd != "": + logrus.Debugf("found dependency:\n %v", result.HclToAdd) ref, err := c.AddHcl(result.HclToAdd, true) if err != nil { return err } c.KnownPatternMap[pattern.String()] = *ref placeHolders[i].Reference = ref + logrus.Debugf("dependency resolved, ref: %v", ref) case result.AzapiDefinitionToAdd != nil: + logrus.Debugf("found dependency:\n %v", result.AzapiDefinitionToAdd) err = c.AddAzapiDefinition(*result.AzapiDefinitionToAdd) if err != nil { return err @@ -147,16 +164,18 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { return fmt.Errorf("resource type address not found: %v after adding azapi definition to the context, azapi def: %v", pattern, result.AzapiDefinitionToAdd) } placeHolders[i].Reference = &ref + logrus.Debugf("dependency resolved, ref: %v", ref) } break } } // replace the id placeholders with the dependency address + logrus.Debugf("replacing id placeholders with dependency address...") for _, filed := range rootFields { for _, placeHolder := range placeHolders { if placeHolder.ValuePath == filed && placeHolder.Reference.IsKnown() { - def.AdditionalFields[filed] = NewReferenceValue(placeHolder.Reference.String()) + def.AdditionalFields[filed] = types.NewReferenceValue(placeHolder.Reference.String()) break } } @@ -178,7 +197,8 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { // add extra dependencies if def.ResourceName == "azapi_resource_list" { - var ref *Reference + logrus.Debugf("adding extra dependencies for azapi_resource_list...") + var ref *types.Reference for pattern, r := range c.KnownPatternMap { if strings.HasSuffix(pattern, strings.ToLower(":"+def.AzureResourceType)) { ref = &r @@ -190,7 +210,9 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { if ref.Kind == "data" { addr = fmt.Sprintf(`data.%s.%s`, ref.Name, ref.Label) } - def.AdditionalFields["depends_on"] = NewRawValue(fmt.Sprintf(`[%s]`, addr)) + def.AdditionalFields["depends_on"] = types.NewRawValue(fmt.Sprintf(`[%s]`, addr)) + } else { + logrus.Debugf("no `depends_on` dependency found for azapi_resource_list: %v", def) } } @@ -201,11 +223,13 @@ func (c *Context) AddAzapiDefinition(input AzapiDefinition) error { if def.AdditionalFields["action"] == nil && def.ResourceName != "azapi_resource_list" { pattern := dependency.NewPattern(def.Id) c.KnownPatternMap[pattern.String()] = *ref + logrus.Debugf("adding known pattern: %s, ref: %s", pattern, *ref) } return nil } -func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*Reference, error) { +func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*types.Reference, error) { + logrus.Debugf("adding hcl:\n%v, skipWhenDuplicate: %v", input, skipWhenDuplicate) inputFile, diags := hclwrite.ParseConfig([]byte(input), "", hcl.InitialPos) if diags.HasErrors() { logrus.Warnf("failed to parse input:\n%v", input) @@ -245,6 +269,7 @@ func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*Reference, erro if conflictBlock == nil { continue } + logrus.Debugf("found conflict: %s %v", conflictBlock.Type(), conflictBlock.Labels()) // if the resource types are the same, there is no conflict if utils.TypeValue(block) == utils.TypeValue(conflictBlock) && skipWhenDuplicate { continue @@ -257,6 +282,7 @@ func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*Reference, erro break } } + logrus.Debugf("renaming labels: %v -> %v", labels[1], newLabel) block.SetLabels([]string{labels[0], newLabel}) input = string(inputFile.BuildTokens(nil).Bytes()) @@ -376,7 +402,7 @@ func (c *Context) AddHcl(input string, skipWhenDuplicate bool) (*Reference, erro if len(labels) != 2 { return nil, fmt.Errorf("label is invalid: %v, input:\n%v", labels, input) } - return &Reference{ + return &types.Reference{ Label: labels[1], Kind: lastBlock.Type(), Name: labels[0], @@ -391,16 +417,16 @@ func (c *Context) String() string { } // GetKeyValueMappings returns a list of key and value of input -func GetKeyValueMappings(parameters interface{}, path string) []PropertyDependencyMapping { +func GetKeyValueMappings(parameters interface{}, path string) []types.PropertyDependencyMapping { if parameters == nil { - return []PropertyDependencyMapping{} + return []types.PropertyDependencyMapping{} } - results := make([]PropertyDependencyMapping, 0) + results := make([]types.PropertyDependencyMapping, 0) switch param := parameters.(type) { case map[string]interface{}: for key, value := range param { results = append(results, GetKeyValueMappings(value, path+"."+key)...) - results = append(results, PropertyDependencyMapping{ + results = append(results, types.PropertyDependencyMapping{ ValuePath: path + "." + key, LiteralValue: key, IsKey: true, @@ -411,7 +437,7 @@ func GetKeyValueMappings(parameters interface{}, path string) []PropertyDependen results = append(results, GetKeyValueMappings(value, path+"."+strconv.Itoa(index))...) } case string: - results = append(results, PropertyDependencyMapping{ + results = append(results, types.PropertyDependencyMapping{ ValuePath: path, LiteralValue: param, IsKey: false, diff --git a/resource/context_test.go b/resource/context_test.go index fc57890c..6c9fdffe 100644 --- a/resource/context_test.go +++ b/resource/context_test.go @@ -1,13 +1,13 @@ -package res_test +package resource_test import ( "testing" - "github.com/ms-henglu/armstrong/res" + "github.com/ms-henglu/armstrong/resource" ) func Test_NewContextInit(t *testing.T) { - context := res.NewContext(nil) + context := resource.NewContext(nil) expected := `terraform { required_providers { azapi = { @@ -29,7 +29,6 @@ variable "location" { type = string default = "westeurope" } - ` actual := context.String() if actual != expected { diff --git a/resource/from_example.go b/resource/from_example.go index 7718ed10..719fcdc0 100644 --- a/resource/from_example.go +++ b/resource/from_example.go @@ -1,16 +1,18 @@ -package res +package resource import ( + "encoding/json" "os" - "encoding/json" + "github.com/ms-henglu/armstrong/resource/types" "github.com/ms-henglu/armstrong/utils" + "github.com/sirupsen/logrus" ) -func NewAzapiDefinitionFromExample(exampleFilepath string, kind string) (AzapiDefinition, error) { +func NewAzapiDefinitionFromExample(exampleFilepath string, kind string) (types.AzapiDefinition, error) { data, err := os.ReadFile(exampleFilepath) if err != nil { - return AzapiDefinition{}, err + return types.AzapiDefinition{}, err } var example struct { Parameters map[string]interface{} `json:"parameters"` @@ -22,44 +24,74 @@ func NewAzapiDefinitionFromExample(exampleFilepath string, kind string) (AzapiDe } err = json.Unmarshal(data, &example) if err != nil { - return AzapiDefinition{}, err + return types.AzapiDefinition{}, err } var body interface{} + locationValue := "" if kind == "resource" && example.Parameters != nil { for _, value := range example.Parameters { if bodyMap, ok := value.(map[string]interface{}); ok { + logrus.Debugf("found request body from example: %v", bodyMap) + if location := bodyMap["location"]; location != nil { + locationValue = location.(string) + delete(bodyMap, "location") + } + if name := bodyMap["name"]; name != nil { + delete(bodyMap, "name") + } + delete(bodyMap, "id") body = bodyMap + break } } + if body == nil { + logrus.Warnf("found no request body from example") + } } var id string for _, statusCode := range []string{"200", "201", "202"} { if response, ok := example.Responses[statusCode]; ok && response.Body.Id != "" { + logrus.Debugf("found id from %s response: %s", statusCode, response.Body.Id) id = response.Body.Id + break } } + if id == "" { + logrus.Warnf("found no id from example") + } resourceType := utils.ResourceTypeOfResourceId(id) + logrus.Debugf("resource type of %s is %s", id, resourceType) var apiVersion string if example.Parameters != nil && example.Parameters["api-version"] != nil { apiVersion = example.Parameters["api-version"].(string) } - return AzapiDefinition{ + out := types.AzapiDefinition{ Id: id, - Kind: Kind(kind), + Kind: types.Kind(kind), ResourceName: "azapi_resource", Label: defaultLabel(resourceType), AzureResourceType: resourceType, ApiVersion: apiVersion, Body: body, - AdditionalFields: map[string]Value{ - "parent_id": NewStringLiteralValue(utils.ParentIdOfResourceId(id)), - "name": NewStringLiteralValue(utils.LastSegment(id)), - "schema_validation_enabled": NewRawValue("false"), + BodyFormat: types.BodyFormatHcl, + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue(utils.ParentIdOfResourceId(id)), + "name": types.NewStringLiteralValue(utils.LastSegment(id)), }, - }, nil + } + + if kind == "resource" { + out.AdditionalFields["schema_validation_enabled"] = types.NewRawValue("false") + out.AdditionalFields["ignore_missing_property"] = types.NewRawValue("false") + out.AdditionalFields["ignore_casing"] = types.NewRawValue("false") + if locationValue != "" { + out.AdditionalFields["location"] = types.NewStringLiteralValue(locationValue) + } + } + return out, nil } diff --git a/resource/from_example_test.go b/resource/from_example_test.go index 958e3540..b78ad986 100644 --- a/resource/from_example_test.go +++ b/resource/from_example_test.go @@ -1 +1,103 @@ -package resource +package resource_test + +import ( + "reflect" + "testing" + + "github.com/ms-henglu/armstrong/resource" + "github.com/ms-henglu/armstrong/resource/types" +) + +func Test_NewAzapiDefinitionFromExample(t *testing.T) { + testcases := []struct { + Input string + Kind string + Want types.AzapiDefinition + ExpectError bool + }{ + { + Input: "testdata/examples/createOrUpdateAutomationAccount.json", + Kind: "resource", + Want: types.AzapiDefinition{ + AzureResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + BodyFormat: types.BodyFormatHcl, + Body: map[string]interface{}{ + "properties": map[string]interface{}{ + "sku": map[string]interface{}{ + "name": "Free", + }, + }, + }, + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue("/subscriptions/subid/resourceGroups/rg"), + "name": types.NewStringLiteralValue("myAutomationAccount9"), + "location": types.NewStringLiteralValue("East US 2"), + "schema_validation_enabled": types.NewRawValue("false"), + "ignore_casing": types.NewRawValue("false"), + "ignore_missing_property": types.NewRawValue("false"), + }, + ResourceName: "azapi_resource", + Label: "automationAccount", + Kind: types.KindResource, + }, + ExpectError: false, + }, + { + Input: "testdata/examples/getAutomationAccount.json", + Kind: "data", + Want: types.AzapiDefinition{ + AzureResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + BodyFormat: types.BodyFormatHcl, + Body: nil, + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue("/subscriptions/subid/resourceGroups/rg"), + "name": types.NewStringLiteralValue("myAutomationAccount9"), + }, + ResourceName: "azapi_resource", + Label: "automationAccount", + Kind: types.KindDataSource, + }, + }, + } + + for _, tc := range testcases { + t.Logf("test case: %s", tc.Input) + got, err := resource.NewAzapiDefinitionFromExample(tc.Input, tc.Kind) + if tc.ExpectError != (err != nil) { + t.Errorf("got error %v, want error %v", err, tc.ExpectError) + continue + } + if got.AzureResourceType != tc.Want.AzureResourceType { + t.Errorf("got %s, want %s", got.AzureResourceType, tc.Want.AzureResourceType) + } + if got.ApiVersion != tc.Want.ApiVersion { + t.Errorf("got %s, want %s", got.ApiVersion, tc.Want.ApiVersion) + } + if got.BodyFormat != tc.Want.BodyFormat { + t.Errorf("got %s, want %s", got.BodyFormat, tc.Want.BodyFormat) + } + if !reflect.DeepEqual(got.Body, tc.Want.Body) { + t.Errorf("got %v, want %v", got.Body, tc.Want.Body) + } + if len(got.AdditionalFields) != len(tc.Want.AdditionalFields) { + t.Errorf("got %v, want %v", got.AdditionalFields, tc.Want.AdditionalFields) + continue + } + for key, value := range got.AdditionalFields { + if tc.Want.AdditionalFields[key] != value { + t.Errorf("got %v, want %v", got.AdditionalFields, tc.Want.AdditionalFields) + } + } + if got.ResourceName != tc.Want.ResourceName { + t.Errorf("got %s, want %s", got.ResourceName, tc.Want.ResourceName) + } + if got.Label != tc.Want.Label { + t.Errorf("got %s, want %s", got.Label, tc.Want.Label) + } + if got.Kind != tc.Want.Kind { + t.Errorf("got %s, want %s", got.Kind, tc.Want.Kind) + } + } +} diff --git a/resource/from_swagger.go b/resource/from_swagger.go index 4feec9a6..59074f99 100644 --- a/resource/from_swagger.go +++ b/resource/from_swagger.go @@ -3,12 +3,12 @@ package resource import ( "encoding/json" "fmt" - "github.com/ms-henglu/armstrong/resource/types" "net/http" "os" "strings" pluralize "github.com/gertd/go-pluralize" + "github.com/ms-henglu/armstrong/resource/types" "github.com/ms-henglu/armstrong/swagger" "github.com/ms-henglu/armstrong/utils" "github.com/sirupsen/logrus" @@ -17,7 +17,7 @@ import ( var pluralizeClient = pluralize.NewClient() -func Format(apiPath swagger.ApiPath) []types.AzapiDefinition { +func NewAzapiDefinitionsFromSwagger(apiPath swagger.ApiPath) []types.AzapiDefinition { methodMap := make(map[string]bool) for _, method := range apiPath.Methods { methodMap[method] = true diff --git a/resource/from_swagger_test.go b/resource/from_swagger_test.go index 919a1499..bf13006c 100644 --- a/resource/from_swagger_test.go +++ b/resource/from_swagger_test.go @@ -347,7 +347,7 @@ func Test_Format(t *testing.T) { } for _, testcase := range testcases { log.Printf("[DEBUG] Testing path: %v", testcase.ApiPath.Path) - actuals := resource.Format(testcase.ApiPath) + actuals := resource.NewAzapiDefinitionsFromSwagger(testcase.ApiPath) expecteds := testcase.Expected if len(actuals) != len(expecteds) { t.Errorf("expected %d definitions, got %d", len(expecteds), len(actuals)) diff --git a/resource/resolver/azapi_definition_resolver.go b/resource/resolver/azapi_definition_resolver.go index d356a7b7..a2a4689f 100644 --- a/resource/resolver/azapi_definition_resolver.go +++ b/resource/resolver/azapi_definition_resolver.go @@ -1 +1,37 @@ package resolver + +import ( + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" +) + +var _ ReferenceResolver = &AzapiDefinitionResolver{} + +type AzapiDefinitionResolver struct { + AzapiDefinitions []types.AzapiDefinition +} + +func (r AzapiDefinitionResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + for _, def := range r.AzapiDefinitions { + if def.AdditionalFields["action"] != nil { + continue + } + if def.ResourceName == "azapi_resource_list" { + continue + } + if strings.EqualFold(def.AzureResourceType, pattern.AzureResourceType) { + return &ResolvedResult{ + AzapiDefinitionToAdd: &def, + }, nil + } + } + return nil, nil +} + +func NewAzapiDefinitionResolver(azapiDefinitions []types.AzapiDefinition) AzapiDefinitionResolver { + return AzapiDefinitionResolver{ + AzapiDefinitions: azapiDefinitions, + } +} diff --git a/resource/resolver/azapi_dependency_resolver.go b/resource/resolver/azapi_dependency_resolver.go index d356a7b7..a73e69ad 100644 --- a/resource/resolver/azapi_dependency_resolver.go +++ b/resource/resolver/azapi_dependency_resolver.go @@ -1 +1,37 @@ package resolver + +import ( + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/sirupsen/logrus" +) + +var _ ReferenceResolver = &AzapiDependencyResolver{} + +type AzapiDependencyResolver struct { + dependencies []dependency.Dependency +} + +func (r AzapiDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + for _, dep := range r.dependencies { + if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { + return &ResolvedResult{ + HclToAdd: dep.ExampleConfiguration, + }, nil + } + } + return nil, nil +} + +func NewAzapiDependencyResolver() AzapiDependencyResolver { + azapiDeps, err := dependency.LoadAzapiDependencies() + if err != nil { + logrus.Fatalf("loading azapi dependencies: %+v", err) + return AzapiDependencyResolver{} + } + logrus.Debugf("loaded %d azapi dependencies", len(azapiDeps)) + return AzapiDependencyResolver{ + dependencies: azapiDeps, + } +} diff --git a/resource/resolver/azapi_resource_id_resolver.go b/resource/resolver/azapi_resource_id_resolver.go index d356a7b7..599dd264 100644 --- a/resource/resolver/azapi_resource_id_resolver.go +++ b/resource/resolver/azapi_resource_id_resolver.go @@ -1 +1,39 @@ package resolver + +import ( + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/utils" +) + +var _ ReferenceResolver = &AzapiResourceIdResolver{} + +type AzapiResourceIdResolver struct { +} + +func (r AzapiResourceIdResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + if !strings.Contains(pattern.AzureResourceType, "/") { + return nil, nil + } + return &ResolvedResult{ + AzapiDefinitionToAdd: &types.AzapiDefinition{ + Id: pattern.Placeholder, + Kind: "data", + ResourceName: "azapi_resource_id", + BodyFormat: types.BodyFormatHcl, + Label: pluralizeClient.Singular(utils.LastSegment(pattern.AzureResourceType)), + AzureResourceType: pattern.AzureResourceType, + ApiVersion: "2023-12-12", + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), + "name": types.NewStringLiteralValue(utils.LastSegment(pattern.Placeholder)), + }, + }, + }, nil +} + +func NewAzapiResourceIdResolver() AzapiResourceIdResolver { + return AzapiResourceIdResolver{} +} diff --git a/resource/resolver/azapi_resource_placeholder_resolver.go b/resource/resolver/azapi_resource_placeholder_resolver.go index d356a7b7..474cdffe 100644 --- a/resource/resolver/azapi_resource_placeholder_resolver.go +++ b/resource/resolver/azapi_resource_placeholder_resolver.go @@ -1 +1,45 @@ package resolver + +import ( + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/utils" +) + +var _ ReferenceResolver = &AzapiResourcePlaceholderResolver{} + +type AzapiResourcePlaceholderResolver struct { +} + +func (a AzapiResourcePlaceholderResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + if !strings.Contains(pattern.AzureResourceType, "/") { + return nil, nil + } + return &ResolvedResult{ + AzapiDefinitionToAdd: &types.AzapiDefinition{ + Id: pattern.Placeholder, + Kind: "resource", + ResourceName: "azapi_resource", + Label: pluralizeClient.Singular(utils.LastSegment(pattern.AzureResourceType)), + AzureResourceType: pattern.AzureResourceType, + BodyFormat: types.BodyFormatHcl, + ApiVersion: "TODO", + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), + "name": types.NewStringLiteralValue(utils.LastSegment(pattern.Placeholder)), + "schema_validation_enabled": types.NewRawValue("false"), + }, + Body: map[string]interface{}{ + "properties": map[string]interface{}{ + "TODO": "TODO", + }, + }, + }, + }, nil +} + +func NewAzapiResourcePlaceholderResolver() AzapiResourcePlaceholderResolver { + return AzapiResourcePlaceholderResolver{} +} diff --git a/resource/resolver/azurerm_dependency_resolver.go b/resource/resolver/azurerm_dependency_resolver.go index d356a7b7..874fc5ad 100644 --- a/resource/resolver/azurerm_dependency_resolver.go +++ b/resource/resolver/azurerm_dependency_resolver.go @@ -1 +1,33 @@ package resolver + +import ( + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/sirupsen/logrus" +) + +var _ ReferenceResolver = &AzurermDependencyResolver{} + +type AzurermDependencyResolver struct { + Dependencies []dependency.Dependency +} + +func (r AzurermDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + for _, dep := range r.Dependencies { + if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { + return &ResolvedResult{ + HclToAdd: dep.ExampleConfiguration, + }, nil + } + } + return nil, nil +} + +func NewAzurermDependencyResolver() AzurermDependencyResolver { + deps := dependency.LoadAzurermDependencies() + logrus.Debugf("loaded %d azurerm dependencies", len(deps)) + return AzurermDependencyResolver{ + Dependencies: deps, + } +} diff --git a/resource/resolver/existing_dependency_resolver.go b/resource/resolver/existing_dependency_resolver.go index d356a7b7..3f05ed19 100644 --- a/resource/resolver/existing_dependency_resolver.go +++ b/resource/resolver/existing_dependency_resolver.go @@ -1 +1,109 @@ package resolver + +import ( + "os" + "path" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/utils" + "github.com/sirupsen/logrus" +) + +var _ ReferenceResolver = &ExistingDependencyResolver{} + +type ExistingDependencyResolver struct { + ExistingDependencies []dependency.Dependency +} + +func (r ExistingDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + for _, dep := range r.ExistingDependencies { + if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { + return &ResolvedResult{ + Reference: &types.Reference{ + Label: dep.ResourceLabel, + Kind: dep.ResourceKind, + Name: dep.ResourceName, + Property: dep.ReferredProperty, + }, + }, nil + } + } + return nil, nil +} + +func NewExistingDependencyResolver(workingDirectory string) ExistingDependencyResolver { + azurermDeps := dependency.LoadAzurermDependencies() + azurermTypeAzureResourceTypeMap := make(map[string]string) + for _, dep := range azurermDeps { + azurermTypeAzureResourceTypeMap[dep.ResourceName] = dep.AzureResourceType + } + files, err := os.ReadDir(workingDirectory) + if err != nil { + logrus.Warnf("reading dir %s: %+v", workingDirectory, err) + return ExistingDependencyResolver{} + } + existDeps := make([]dependency.Dependency, 0) + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".tf") { + continue + } + src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) + if err != nil { + logrus.Warnf("reading file %s: %+v", file.Name(), err) + continue + } + f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos) + if diag.HasErrors() { + logrus.Warnf("parsing file %s: %+v", file.Name(), diag.Error()) + continue + } + if f == nil || f.Body() == nil { + logrus.Debugf("empty file %s", file.Name()) + continue + } + for _, block := range f.Body().Blocks() { + labels := block.Labels() + if len(labels) >= 2 { + switch { + case strings.HasPrefix(labels[0], "azapi_"): + typeValue := utils.AttributeValue(block.Body().GetAttribute("type")) + parts := strings.Split(typeValue, "@") + if len(parts) != 2 { + logrus.Warnf("invalid type value %s (labels: %v, filename: %s)", typeValue, labels, file.Name()) + continue + } + logrus.Debugf("found existing azapi dependency: %s (labels: %v, filename: %s)", parts[0], labels, file.Name()) + existDeps = append(existDeps, dependency.Dependency{ + AzureResourceType: parts[0], + ApiVersion: parts[1], + ExampleConfiguration: string(block.BuildTokens(nil).Bytes()), + ResourceKind: block.Type(), + ReferredProperty: "id", + ResourceName: labels[0], + ResourceLabel: labels[1], + }) + case strings.HasPrefix(labels[0], "azurerm_"): + azureResourceType := azurermTypeAzureResourceTypeMap[labels[0]] + logrus.Debugf("found existing azurerm dependency: %s (labels: %v, filename: %s)", azureResourceType, labels, file.Name()) + existDeps = append(existDeps, dependency.Dependency{ + AzureResourceType: azureResourceType, + ApiVersion: "", + ExampleConfiguration: string(block.BuildTokens(nil).Bytes()), + ResourceKind: block.Type(), + ReferredProperty: "id", + ResourceName: labels[0], + ResourceLabel: labels[1], + }) + } + } + } + } + logrus.Infof("found %d existing dependencies", len(existDeps)) + return ExistingDependencyResolver{ + ExistingDependencies: existDeps, + } +} diff --git a/resource/resolver/known_reference_resolver.go b/resource/resolver/known_reference_resolver.go index d356a7b7..052623ec 100644 --- a/resource/resolver/known_reference_resolver.go +++ b/resource/resolver/known_reference_resolver.go @@ -1 +1,28 @@ package resolver + +import ( + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" +) + +var _ ReferenceResolver = &KnownReferenceResolver{} + +type KnownReferenceResolver struct { + knownPatterns map[string]types.Reference +} + +func (r KnownReferenceResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + ref, ok := r.knownPatterns[pattern.String()] + if !ok || !ref.IsKnown() { + return nil, nil + } + return &ResolvedResult{ + Reference: &ref, + }, nil +} + +func NewKnownReferenceResolver(knownPatterns map[string]types.Reference) KnownReferenceResolver { + return KnownReferenceResolver{ + knownPatterns: knownPatterns, + } +} diff --git a/resource/resolver/location_id_resolver.go b/resource/resolver/location_id_resolver.go index d356a7b7..f2fdfbbb 100644 --- a/resource/resolver/location_id_resolver.go +++ b/resource/resolver/location_id_resolver.go @@ -1 +1,38 @@ package resolver + +import ( + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/utils" +) + +var _ ReferenceResolver = &LocationIDResolver{} + +type LocationIDResolver struct { +} + +func (r LocationIDResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + if utils.LastSegment(pattern.AzureResourceType) == "locations" { + azapiDef := types.AzapiDefinition{ + Id: pattern.Placeholder, + Kind: types.KindDataSource, + ResourceName: "azapi_resource_id", + Label: "location", + BodyFormat: types.BodyFormatHcl, + AzureResourceType: pattern.AzureResourceType, + ApiVersion: "2023-12-12", + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), + "name": types.NewReferenceValue("var.location"), + }, + } + return &ResolvedResult{ + AzapiDefinitionToAdd: &azapiDef, + }, nil + } + return nil, nil +} + +func NewLocationIDResolver() LocationIDResolver { + return LocationIDResolver{} +} diff --git a/resource/resolver/provider_id_resolver.go b/resource/resolver/provider_id_resolver.go index d356a7b7..29ca2265 100644 --- a/resource/resolver/provider_id_resolver.go +++ b/resource/resolver/provider_id_resolver.go @@ -1 +1,41 @@ package resolver + +import ( + "fmt" + "strings" + + "github.com/ms-henglu/armstrong/dependency" + "github.com/ms-henglu/armstrong/resource/types" + "github.com/ms-henglu/armstrong/utils" +) + +var _ ReferenceResolver = &ProviderIDResolver{} + +type ProviderIDResolver struct { +} + +func (r ProviderIDResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { + if !strings.Contains(pattern.AzureResourceType, "/") && pattern.Scope != dependency.ScopeTenant { + azapiDef := types.AzapiDefinition{ + Id: pattern.Placeholder, + Kind: types.KindDataSource, + ResourceName: "azapi_resource_id", + Label: fmt.Sprintf("%sScopeProvider", pattern.Scope), + AzureResourceType: "Microsoft.Resources/providers", + ApiVersion: "2020-06-01", + BodyFormat: types.BodyFormatHcl, + AdditionalFields: map[string]types.Value{ + "parent_id": types.NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), + "name": types.NewStringLiteralValue(pattern.AzureResourceType), + }, + } + return &ResolvedResult{ + AzapiDefinitionToAdd: &azapiDef, + }, nil + } + return nil, nil +} + +func NewProviderIDResolver() ProviderIDResolver { + return ProviderIDResolver{} +} diff --git a/resource/resolver/reference_resolver.go b/resource/resolver/reference_resolver.go index 891e1e5c..d6f17bfd 100644 --- a/resource/resolver/reference_resolver.go +++ b/resource/resolver/reference_resolver.go @@ -1,306 +1,19 @@ -package resource +package resolver import ( - "fmt" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/sirupsen/logrus" - "os" - "path" - "strings" - + "github.com/gertd/go-pluralize" "github.com/ms-henglu/armstrong/dependency" - "github.com/ms-henglu/armstrong/utils" + "github.com/ms-henglu/armstrong/resource/types" ) +var pluralizeClient = pluralize.NewClient() + type ResolvedResult struct { - Reference *Reference + Reference *types.Reference HclToAdd string - AzapiDefinitionToAdd *AzapiDefinition + AzapiDefinitionToAdd *types.AzapiDefinition } type ReferenceResolver interface { Resolve(pattern dependency.Pattern) (*ResolvedResult, error) } - -var _ ReferenceResolver = &KnownReferenceResolver{} - -type KnownReferenceResolver struct { - knownPatterns map[string]Reference -} - -func (r KnownReferenceResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - ref, ok := r.knownPatterns[pattern.String()] - if !ok || !ref.IsKnown() { - return nil, nil - } - return &ResolvedResult{ - Reference: &ref, - }, nil -} - -func NewKnownReferenceResolver(knownPatterns map[string]Reference) KnownReferenceResolver { - return KnownReferenceResolver{ - knownPatterns: knownPatterns, - } -} - -var _ ReferenceResolver = &AzapiDependencyResolver{} - -type AzapiDependencyResolver struct { - dependencies []dependency.Dependency -} - -func (r AzapiDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - for _, dep := range r.dependencies { - if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { - return &ResolvedResult{ - HclToAdd: dep.ExampleConfiguration, - }, nil - } - } - return nil, nil -} - -func NewAzapiDependencyResolver() AzapiDependencyResolver { - logrus.Debugf("loading azapi dependencies...") - azapiDeps, err := dependency.LoadAzapiDependencies() - if err != nil { - logrus.Fatalf("loading azapi dependencies: %+v", err) - return AzapiDependencyResolver{} - } - return AzapiDependencyResolver{ - dependencies: azapiDeps, - } -} - -var _ ReferenceResolver = &AzapiDefinitionResolver{} - -type AzapiDefinitionResolver struct { - AzapiDefinitions []AzapiDefinition -} - -func (r AzapiDefinitionResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - for _, def := range r.AzapiDefinitions { - if def.AdditionalFields["action"] != nil { - continue - } - if def.ResourceName == "azapi_resource_list" { - continue - } - if strings.EqualFold(def.AzureResourceType, pattern.AzureResourceType) { - return &ResolvedResult{ - AzapiDefinitionToAdd: &def, - }, nil - } - } - return nil, nil -} - -func NewAzapiDefinitionResolver(azapiDefinitions []AzapiDefinition) AzapiDefinitionResolver { - return AzapiDefinitionResolver{ - AzapiDefinitions: azapiDefinitions, - } -} - -var _ ReferenceResolver = &ProviderIDResolver{} - -type ProviderIDResolver struct { -} - -func (r ProviderIDResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - if !strings.Contains(pattern.AzureResourceType, "/") && pattern.Scope != dependency.ScopeTenant { - azapiDef := AzapiDefinition{ - Id: pattern.Placeholder, - Kind: KindDataSource, - ResourceName: "azapi_resource_id", - Label: fmt.Sprintf("%sScopeProvider", pattern.Scope), - AzureResourceType: "Microsoft.Resources/providers", - ApiVersion: "2020-06-01", - AdditionalFields: map[string]Value{ - "parent_id": NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), - "name": NewStringLiteralValue(pattern.AzureResourceType), - }, - } - return &ResolvedResult{ - AzapiDefinitionToAdd: &azapiDef, - }, nil - } - return nil, nil -} - -func NewProviderIDResolver() ProviderIDResolver { - return ProviderIDResolver{} -} - -var _ ReferenceResolver = &LocationIDResolver{} - -type LocationIDResolver struct { -} - -func (r LocationIDResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - if utils.LastSegment(pattern.AzureResourceType) == "locations" { - azapiDef := AzapiDefinition{ - Id: pattern.Placeholder, - Kind: KindDataSource, - ResourceName: "azapi_resource_id", - Label: "location", - AzureResourceType: pattern.AzureResourceType, - ApiVersion: "2023-12-12", - AdditionalFields: map[string]Value{ - "parent_id": NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), - "name": NewReferenceValue("var.location"), - }, - } - return &ResolvedResult{ - AzapiDefinitionToAdd: &azapiDef, - }, nil - } - return nil, nil -} - -func NewLocationIDResolver() LocationIDResolver { - return LocationIDResolver{} -} - -var _ ReferenceResolver = &AzapiResourceIdResolver{} - -type AzapiResourceIdResolver struct { -} - -func (r AzapiResourceIdResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - if !strings.Contains(pattern.AzureResourceType, "/") { - return nil, nil - } - return &ResolvedResult{ - AzapiDefinitionToAdd: &AzapiDefinition{ - Id: pattern.Placeholder, - Kind: "data", - ResourceName: "azapi_resource_id", - Label: pluralizeClient.Singular(utils.LastSegment(pattern.AzureResourceType)), - AzureResourceType: pattern.AzureResourceType, - ApiVersion: "2023-12-12", - AdditionalFields: map[string]Value{ - "parent_id": NewStringLiteralValue(utils.ParentIdOfResourceId(pattern.Placeholder)), - "name": NewStringLiteralValue(utils.LastSegment(pattern.Placeholder)), - }, - }, - }, nil -} - -func NewAzapiResourceIdResolver() AzapiResourceIdResolver { - return AzapiResourceIdResolver{} -} - -var _ ReferenceResolver = &AzurermDependencyResolver{} - -type AzurermDependencyResolver struct { - Dependencies []dependency.Dependency -} - -func (r AzurermDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - for _, dep := range r.Dependencies { - if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { - return &ResolvedResult{ - HclToAdd: dep.ExampleConfiguration, - }, nil - } - } - return nil, nil -} - -func NewAzurermDependencyResolver() AzurermDependencyResolver { - return AzurermDependencyResolver{ - Dependencies: dependency.LoadAzurermDependencies(), - } -} - -var _ ReferenceResolver = &ExistingDependencyResolver{} - -type ExistingDependencyResolver struct { - ExistingDependencies []dependency.Dependency -} - -func (r ExistingDependencyResolver) Resolve(pattern dependency.Pattern) (*ResolvedResult, error) { - for _, dep := range r.ExistingDependencies { - if strings.EqualFold(dep.AzureResourceType, pattern.AzureResourceType) { - return &ResolvedResult{ - Reference: &Reference{ - Label: dep.ResourceLabel, - Kind: dep.ResourceKind, - Name: dep.ResourceName, - Property: dep.ReferredProperty, - }, - }, nil - } - } - return nil, nil -} - -func NewExistingDependencyResolver(workingDirectory string) ExistingDependencyResolver { - azurermDeps := dependency.LoadAzurermDependencies() - azurermTypeAzureResourceTypeMap := make(map[string]string) - for _, dep := range azurermDeps { - azurermTypeAzureResourceTypeMap[dep.ResourceName] = dep.AzureResourceType - } - files, err := os.ReadDir(workingDirectory) - if err != nil { - logrus.Warnf("reading dir %s: %+v", workingDirectory, err) - return ExistingDependencyResolver{} - } - existDeps := make([]dependency.Dependency, 0) - for _, file := range files { - if !strings.HasSuffix(file.Name(), ".tf") { - continue - } - src, err := os.ReadFile(path.Join(workingDirectory, file.Name())) - if err != nil { - logrus.Warnf("reading file %s: %+v", file.Name(), err) - continue - } - f, diag := hclwrite.ParseConfig(src, file.Name(), hcl.InitialPos) - if diag.HasErrors() { - logrus.Warnf("parsing file %s: %+v", file.Name(), diag.Error()) - continue - } - if f == nil || f.Body() == nil { - continue - } - for _, block := range f.Body().Blocks() { - labels := block.Labels() - if len(labels) >= 2 { - switch { - case strings.HasPrefix(labels[0], "azapi_"): - typeValue := utils.AttributeValue(block.Body().GetAttribute("type")) - parts := strings.Split(typeValue, "@") - if len(parts) != 2 { - continue - } - existDeps = append(existDeps, dependency.Dependency{ - AzureResourceType: parts[0], - ApiVersion: parts[1], - ExampleConfiguration: string(block.BuildTokens(nil).Bytes()), - ResourceKind: block.Type(), - ReferredProperty: "id", - ResourceName: labels[0], - ResourceLabel: labels[1], - }) - case strings.HasPrefix(labels[0], "azurerm_"): - azureResourceType := azurermTypeAzureResourceTypeMap[labels[0]] - existDeps = append(existDeps, dependency.Dependency{ - AzureResourceType: azureResourceType, - ApiVersion: "", - ExampleConfiguration: string(block.BuildTokens(nil).Bytes()), - ResourceKind: block.Type(), - ReferredProperty: "id", - ResourceName: labels[0], - ResourceLabel: labels[1], - }) - } - } - } - } - return ExistingDependencyResolver{ - ExistingDependencies: existDeps, - } -} diff --git a/resource/testdata/examples/createOrUpdateAutomationAccount.json b/resource/testdata/examples/createOrUpdateAutomationAccount.json new file mode 100644 index 00000000..4b065458 --- /dev/null +++ b/resource/testdata/examples/createOrUpdateAutomationAccount.json @@ -0,0 +1,63 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount9", + "api-version": "2022-08-08", + "parameters": { + "properties": { + "sku": { + "name": "Free" + } + }, + "name": "myAutomationAccount9", + "location": "East US 2" + } + }, + "responses": { + "201": { + "headers": {}, + "body": { + "name": "myAutomationAccount9", + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount9", + "type": "Microsoft.Automation/AutomationAccounts", + "location": "East US 2", + "tags": {}, + "etag": null, + "properties": { + "sku": { + "name": "Free", + "family": null, + "capacity": null + }, + "state": "Ok", + "creationTime": "2017-03-26T01:13:43.267+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "lastModifiedTime": "2017-03-26T01:13:43.267+00:00" + } + } + }, + "200": { + "headers": {}, + "body": { + "name": "ContoseAutomationAccount", + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount9", + "type": "Microsoft.Automation/AutomationAccounts", + "location": "East US 2", + "tags": {}, + "etag": null, + "properties": { + "sku": { + "name": "Free", + "family": null, + "capacity": null + }, + "state": "Ok", + "creationTime": "2017-03-28T18:21:15.187+00:00", + "lastModifiedBy": "myEmaild@microsoft.com", + "lastModifiedTime": "2017-03-28T18:21:15.187+00:00" + } + } + } + } +} diff --git a/resource/testdata/examples/deleteAutomationAccount.json b/resource/testdata/examples/deleteAutomationAccount.json new file mode 100644 index 00000000..bb258de7 --- /dev/null +++ b/resource/testdata/examples/deleteAutomationAccount.json @@ -0,0 +1,12 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount9", + "api-version": "2022-08-08" + }, + "responses": { + "200": {}, + "204": {} + } +} diff --git a/resource/testdata/examples/deserializeGraphRunbookContent.json b/resource/testdata/examples/deserializeGraphRunbookContent.json new file mode 100644 index 00000000..ecd78bc0 --- /dev/null +++ b/resource/testdata/examples/deserializeGraphRunbookContent.json @@ -0,0 +1,28 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "MyAutomationAccount", + "api-version": "2021-06-22", + "parameters": { + "rawContent": { + "schemaVersion": "1.10", + "runbookDefinition": "AAEAAADAQAAAAAAAAAMAgAAAGJPcmNoZXN0cmF0b3IuR3JhcGhSdW5ib29rLk1vZGVsLCBWZXJzaW9uPTcuMy4wLjAsIEN1bHR....", + "runbookType": "GraphPowerShell" + } + } + }, + "responses": { + "200": { + "headers": {}, + "body": { + "rawContent": { + "schemaVersion": "1.10", + "runbookDefinition": "AAEAAADAQAAAAAAAAAMAgAAAGJPcmNoZXN0cmF0b3IuR3JhcGhSdW5ib29rLk1vZGVsLCBWZXJzaW9uPTcuMy4wLjAsIEN1bHR....", + "runbookType": "GraphPowerShell" + }, + "graphRunbookJson": "" + } + } + } +} diff --git a/resource/testdata/examples/getAutomationAccount.json b/resource/testdata/examples/getAutomationAccount.json new file mode 100644 index 00000000..bee4eca1 --- /dev/null +++ b/resource/testdata/examples/getAutomationAccount.json @@ -0,0 +1,32 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount9", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "name": "myAutomationAccount9", + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount9", + "type": "Microsoft.Automation/AutomationAccounts", + "location": "East US 2", + "tags": {}, + "etag": null, + "properties": { + "sku": { + "name": "Free", + "family": null, + "capacity": null + }, + "state": "Ok", + "creationTime": "2017-03-26T01:13:43.267+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "lastModifiedTime": "2017-03-26T01:13:43.267+00:00" + } + } + } + } +} diff --git a/resource/testdata/examples/getStatisticsOfAutomationAccount.json b/resource/testdata/examples/getStatisticsOfAutomationAccount.json new file mode 100644 index 00000000..4cda2c21 --- /dev/null +++ b/resource/testdata/examples/getStatisticsOfAutomationAccount.json @@ -0,0 +1,108 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount11", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "value": [ + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/New", + "counterProperty": "New", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Activating", + "counterProperty": "Activating", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Running", + "counterProperty": "Running", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Completed", + "counterProperty": "Completed", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Failed", + "counterProperty": "Failed", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Stopped", + "counterProperty": "Stopped", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Blocked", + "counterProperty": "Blocked", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Suspended", + "counterProperty": "Suspended", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Disconnected", + "counterProperty": "Disconnected", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Suspending", + "counterProperty": "Suspending", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Stopping", + "counterProperty": "Stopping", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Resuming", + "counterProperty": "Resuming", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11/statistics/Removing", + "counterProperty": "Removing", + "counterValue": 0, + "startTime": "2017-03-19T02:11:49.9879197+00:00", + "endTime": "2017-03-26T02:11:49.9879197+00:00" + } + ] + } + } + } +} diff --git a/resource/testdata/examples/getUsagesOfAutomationAccount.json b/resource/testdata/examples/getUsagesOfAutomationAccount.json new file mode 100644 index 00000000..0f8c7aa8 --- /dev/null +++ b/resource/testdata/examples/getUsagesOfAutomationAccount.json @@ -0,0 +1,47 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount11", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "value": [ + { + "name": { + "value": "AccountUsage", + "localizedValue": "AccountUsage" + }, + "unit": "Minute", + "currentValue": 0.0, + "limit": 500, + "throttleStatus": "NotThrottled" + }, + { + "name": { + "value": "SubscriptionUsage", + "localizedValue": "SubscriptionUsage" + }, + "unit": "Minute", + "currentValue": 429.0, + "limit": 500, + "throttleStatus": "NotThrottled" + }, + { + "name": { + "value": "DscSubscriptionUsage", + "localizedValue": "DscSubscriptionUsage" + }, + "unit": "Count", + "currentValue": 8.0, + "limit": 5, + "throttleStatus": "ThrottledAtSubscriptionLevel" + } + ] + } + } + } +} diff --git a/resource/testdata/examples/listAutomationAccountKeys.json b/resource/testdata/examples/listAutomationAccountKeys.json new file mode 100644 index 00000000..44effc47 --- /dev/null +++ b/resource/testdata/examples/listAutomationAccountKeys.json @@ -0,0 +1,27 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "MyAutomationAccount", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "keys": [ + { + "KeyName": "Primary", + "Permissions": "Full", + "Value": "**************************************************************" + }, + { + "KeyName": "Secondary", + "Permissions": "Full", + "Value": "**************************************************************" + } + ] + } + } + } +} diff --git a/resource/testdata/examples/listAutomationAccountsByResourceGroup.json b/resource/testdata/examples/listAutomationAccountsByResourceGroup.json new file mode 100644 index 00000000..be9cdaa8 --- /dev/null +++ b/resource/testdata/examples/listAutomationAccountsByResourceGroup.json @@ -0,0 +1,136 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "value": [ + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myaccount", + "location": "eastus2", + "name": "myaccount", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-24T00:47:04.227+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAccount123", + "location": "eastus2", + "name": "myAccount123", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-29T00:32:32.52+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAccountasfads", + "location": "East US 2", + "name": "myAccountasfads", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:21:03.27+00:00", + "lastModifiedTime": "2017-03-24T23:21:03.27+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount1", + "location": "East US 2", + "name": "myAutomationAccount1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:22:33.26+00:00", + "lastModifiedTime": "2017-03-24T23:22:33.26+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11", + "location": "East US 2", + "name": "myAutomationAccount11", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-26T02:10:24.523+00:00", + "lastModifiedTime": "2017-03-26T02:11:12.027+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount2", + "location": "East US 2", + "name": "myAutomationAccount2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:24:20.31+00:00", + "lastModifiedTime": "2017-03-24T23:24:20.31+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount3", + "location": "East US 2", + "name": "myAutomationAccount3", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:24:43.967+00:00", + "lastModifiedTime": "2017-03-24T23:24:43.967+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount4", + "location": "East US 2", + "name": "myAutomationAccount4", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:04:56.9+00:00", + "lastModifiedTime": "2017-03-25T02:04:56.9+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount6", + "location": "East US 2", + "name": "myAutomationAccount6", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:10:44.567+00:00", + "lastModifiedTime": "2017-03-25T02:10:44.567+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount7", + "location": "East US 2", + "name": "myAutomationAccount7", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:19:17.943+00:00", + "lastModifiedTime": "2017-03-25T02:19:17.943+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "state": "Ok" + } + } + ] + } + } + } +} diff --git a/resource/testdata/examples/listAutomationAccountsBySubscription.json b/resource/testdata/examples/listAutomationAccountsBySubscription.json new file mode 100644 index 00000000..80f24675 --- /dev/null +++ b/resource/testdata/examples/listAutomationAccountsBySubscription.json @@ -0,0 +1,685 @@ +{ + "parameters": { + "subscriptionId": "subid", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "value": [ + { + "id": "/subscriptions/subid/resourceGroups/JPEDeploy1/providers/Microsoft.Automation/automationAccounts/JPEDDeployDSC1", + "location": "japaneast", + "name": "JPEDDeployDSC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-28T23:48:25.143+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.06+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/jpeDemo1/providers/Microsoft.Automation/automationAccounts/jpeDemoAutomation1", + "location": "japaneast", + "name": "jpeDemoAutomation1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-25T02:04:10.223+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.06+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/ASERG1/providers/Microsoft.Automation/automationAccounts/ASEAutomationAccount1", + "location": "australiasoutheast", + "name": "ASEAutomationAccount1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-04-12T05:19:19.48+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.64+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/eus2Demo1/providers/Microsoft.Automation/automationAccounts/AAEU2DSCDemo", + "location": "East US 2", + "name": "AAEU2DSCDemo", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-08-04T14:44:02.397+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/eus2Demo1/providers/Microsoft.Automation/automationAccounts/AAEU2DSCDemo2", + "location": "East US 2", + "name": "AAEU2DSCDemo2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-08-04T15:03:45.977+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/aadscdemo1/providers/Microsoft.Automation/automationAccounts/AAsnoverDemo1", + "location": "East US 2", + "name": "AAsnoverDemo1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-01-29T02:29:13.18+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/oaastest/providers/Microsoft.Automation/automationAccounts/automationaccdelete", + "location": "eastus2", + "name": "automationaccdelete", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-03-29T20:30:49.97+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/aadscdemo2/providers/Microsoft.Automation/automationAccounts/mytest1212", + "location": "eastus2", + "name": "mytest1212", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-12-12T20:25:36.34+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/CIDRG/providers/Microsoft.Automation/automationAccounts/deleteacc", + "location": "eastus2", + "name": "deleteacc", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-12T22:13:39.79+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/test/providers/Microsoft.Automation/automationAccounts/deleteme", + "location": "eastus2", + "name": "deleteme", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-12T21:56:10.267+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/tst/providers/Microsoft.Automation/automationAccounts/deleteme3", + "location": "eastus2", + "name": "deleteme3", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-12T22:00:51.333+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/OaaSCSXASVNLMD6CUTP2UKUNHMCSLLJRVOSRAS2HOBKX4B3A3UBNLZWZEA-East-US/providers/Microsoft.Automation/automationAccounts/Eus2Account1", + "location": "East US 2", + "name": "Eus2Account1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-05-19T19:07:43.2+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/EUS2Deploy1/providers/Microsoft.Automation/automationAccounts/EUS2DDeployDSC1", + "location": "eastus2", + "name": "EUS2DDeployDSC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-28T23:50:56.16+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/OaaSCSXASVNLMD6CUTP2UKUNHMCSLLJRVOSRAS2HOBKX4B3A3UBNLZWZEA-East-US/providers/Microsoft.Automation/automationAccounts/eusAccount2", + "location": "eastus2", + "name": "eusAccount2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-05-19T19:12:19.853+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/myProdAutomation1", + "location": "eastus2", + "name": "myProdAutomation1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-02-16T21:31:06.333+00:00", + "lastModifiedTime": "2017-02-16T21:31:06.333+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/myProdDevAutomation", + "location": "eastus2", + "name": "myProdDevAutomation", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-10-27T21:11:16.71+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/myProdPublicAutomation", + "location": "eastus2", + "name": "myProdPublicAutomation", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-11-18T19:49:08.893+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myTestaccount", + "location": "eastus2", + "name": "myTestaccount", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-24T00:47:04.227+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/kjohn-sandbox-eus", + "location": "eastus2", + "name": "kjohn-sandbox-eus", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-19T17:29:18.493+00:00", + "lastModifiedTime": "2017-03-19T17:29:18.493+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/kjohn-sandbox-eus-proddev", + "location": "eastus2", + "name": "kjohn-sandbox-eus-proddev", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-06T02:33:10.29+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/kjohn-rg/providers/Microsoft.Automation/automationAccounts/kjohn-sandbox-eus-prodtest", + "location": "eastus2", + "name": "kjohn-sandbox-eus-prodtest", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-10T23:40:13.103+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/kjohn-rg/providers/Microsoft.Automation/automationAccounts/kjohn-sandbox-eus-prodtest2", + "location": "eastus2", + "name": "kjohn-sandbox-eus-prodtest2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-11T00:20:50.463+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/LinuxPatchingOpsEUS-AA2", + "location": "eastus2", + "name": "LinuxPatchingOpsEUS-AA2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-24T03:17:00.043+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/LinuxTestNewAA", + "location": "eastus2", + "name": "LinuxTestNewAA", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-23T18:50:54.887+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/OaasCSsubid-east-us/providers/Microsoft.Automation/automationAccounts/psrdfeAccount2", + "location": "East US 2", + "name": "psrdfeAccount2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-05-05T00:26:49.02+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/myrg/providers/Microsoft.Automation/automationAccounts/my-account-one", + "location": "eastus2", + "name": "my-account-one", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-10-27T17:54:31.007+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/myku-no-vms", + "location": "eastus2", + "name": "myku-no-vms", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-30T01:12:00.853+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/myku-win-vms", + "location": "eastus2", + "name": "myku-win-vms", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-30T01:17:07.613+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/aadscdemo2/providers/Microsoft.Automation/automationAccounts/SVCPrnAcctTest1", + "location": "eastus2", + "name": "SVCPrnAcctTest1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-03-28T20:12:48.163+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/tesdbgfvfhgjghjgh/providers/Microsoft.Automation/automationAccounts/testfgbhfghfgh", + "location": "eastus2", + "name": "testfgbhfghfgh", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-12-12T20:48:59.3+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-eus/providers/Microsoft.Automation/automationAccounts/test-linux-linuxopsworkspace", + "location": "eastus2", + "name": "test-linux-linuxopsworkspace", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-23T18:59:56.99+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my-dev/providers/Microsoft.Automation/automationAccounts/my-dsc-test-1", + "location": "eastus2", + "name": "my-dsc-test-1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-11-10T00:21:05.133+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my-dev/providers/Microsoft.Automation/automationAccounts/my-test-automation-1", + "location": "eastus2", + "name": "my-test-automation-1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-11-09T19:57:50.043+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/myresourcegroupeus/providers/Microsoft.Automation/automationAccounts/myAccount", + "location": "eastus2", + "name": "myAccount", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-10T19:10:30.453+00:00", + "lastModifiedTime": "2017-03-10T19:10:30.453+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAccount123", + "location": "eastus2", + "name": "myAccount123", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-29T00:32:32.52+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAccountasfads", + "location": "East US 2", + "name": "myAccountasfads", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:21:03.27+00:00", + "lastModifiedTime": "2017-03-24T23:21:03.27+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/myResourceGroupEUS/providers/Microsoft.Automation/automationAccounts/myAccountEUS", + "location": "eastus2", + "name": "myAccountEUS", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-29T23:13:38.873+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.4+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount1", + "location": "East US 2", + "name": "myAutomationAccount1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:22:33.26+00:00", + "lastModifiedTime": "2017-03-24T23:22:33.26+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount11", + "location": "East US 2", + "name": "myAutomationAccount11", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-26T02:10:24.523+00:00", + "lastModifiedTime": "2017-03-26T02:11:12.027+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount2", + "location": "East US 2", + "name": "myAutomationAccount2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:24:20.31+00:00", + "lastModifiedTime": "2017-03-24T23:24:20.31+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount3", + "location": "East US 2", + "name": "myAutomationAccount3", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-24T23:24:43.967+00:00", + "lastModifiedTime": "2017-03-24T23:24:43.967+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount4", + "location": "East US 2", + "name": "myAutomationAccount4", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:04:56.9+00:00", + "lastModifiedTime": "2017-03-25T02:04:56.9+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount6", + "location": "East US 2", + "name": "myAutomationAccount6", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:10:44.567+00:00", + "lastModifiedTime": "2017-03-25T02:10:44.567+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount7", + "location": "East US 2", + "name": "myAutomationAccount7", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-25T02:19:17.943+00:00", + "lastModifiedTime": "2017-03-25T02:19:17.943+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/WEURG1/providers/Microsoft.Automation/automationAccounts/CSSCase1", + "location": "westeurope", + "name": "CSSCase1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-08-18T05:53:58.91+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/deleteme", + "location": "westeurope", + "name": "deleteme", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-01-12T21:48:47.98+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/mms-weu/providers/Microsoft.Automation/automationAccounts/LinuxPatchingOpsWEU-AA2", + "location": "westeurope", + "name": "LinuxPatchingOpsWEU-AA2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-24T02:27:35.713+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my-dev/providers/Microsoft.Automation/automationAccounts/LinuxPatchJobs", + "location": "westeurope", + "name": "LinuxPatchJobs", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-14T22:02:28.223+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/IgnniteRG/providers/Microsoft.Automation/automationAccounts/MyCoolAAC1", + "location": "westeurope", + "name": "MyCoolAAC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-22T05:59:22.443+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/IgnniteRG/providers/Microsoft.Automation/automationAccounts/MyCoolACT1", + "location": "westeurope", + "name": "MyCoolACT1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-22T06:05:18.5+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/OaaSCSXASVNLMD6CUTP2UKUNHMCSLLJRVOSRAS2HOBKX4B3A3UBNLZWZEA-West-Europe/providers/Microsoft.Automation/automationAccounts/MyTestmyTest-WEU", + "location": "West Europe", + "name": "MyTestmyTest-WEU", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2014-11-12T02:48:51.473+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.373+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/WEDeploy1/providers/Microsoft.Automation/automationAccounts/WEDDeployDSC1", + "location": "westeurope", + "name": "WEDDeployDSC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-28T23:20:01.73+00:00", + "lastModifiedTime": "2017-03-13T08:43:47.36+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/SEADeploy1/providers/Microsoft.Automation/automationAccounts/SEADDeployDSC1", + "location": "southeamyia", + "name": "SEADDeployDSC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-28T19:06:39.1+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.683+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/seaDemo1/providers/Microsoft.Automation/automationAccounts/seaDemoAutomation1", + "location": "southeamyia", + "name": "seaDemoAutomation1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-04-25T01:31:32.15+00:00", + "lastModifiedTime": "2017-02-09T21:35:16.683+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/aadscdemo1/providers/Microsoft.Automation/automationAccounts/AAsnoverDemo2", + "location": "South Central US", + "name": "AAsnoverDemo2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-01-29T02:30:05.84+00:00", + "lastModifiedTime": "2017-02-09T21:35:17.107+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/SCUSDeploy1/providers/Microsoft.Automation/automationAccounts/SCUSDDeployDSC1", + "location": "southcentralus", + "name": "SCUSDDeployDSC1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-06-04T23:06:44.897+00:00", + "lastModifiedTime": "2017-02-09T21:35:17.107+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/eus2Demo1/providers/Microsoft.Automation/automationAccounts/scusposthydtest1", + "location": "South Central US", + "name": "scusposthydtest1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-06-04T16:44:18.823+00:00", + "lastModifiedTime": "2017-02-09T21:35:17.107+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/OaaSCSXASVNLMD6CUTP2UKUNHMCSLLJRVOSRAS2HOBKX4B3A3UBNLZWZEA-South-Central-US/providers/Microsoft.Automation/automationAccounts/scustestaccount1", + "location": "South Central US", + "name": "scustestaccount1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2015-05-30T01:54:57.313+00:00", + "lastModifiedTime": "2017-02-09T21:35:17.107+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/UKSRG1/providers/Microsoft.Automation/automationAccounts/AAUKSmyTest1", + "location": "uksouth", + "name": "AAUKSmyTest1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-02-15T02:31:03.19+00:00", + "lastModifiedTime": "2017-02-15T02:31:03.19+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/Gaurav_machines/providers/Microsoft.Automation/automationAccounts/Tip-WCUS-AutomationAccount", + "location": "westcentralus", + "name": "Tip-WCUS-AutomationAccount", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-08T19:53:36.2933333+00:00", + "lastModifiedTime": "2017-03-08T19:53:36.2933333+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/myRG/providers/Microsoft.Automation/automationAccounts/aa-my", + "location": "northeurope", + "name": "aa-my", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-09-21T18:59:56.26+00:00", + "lastModifiedTime": "2017-02-09T21:35:15.457+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/CIDRG/providers/Microsoft.Automation/automationAccounts/CIDAccout1", + "location": "Central India", + "name": "CIDAccout1", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-02-24T20:04:58.867+00:00", + "lastModifiedTime": "2017-02-09T21:35:19.003+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/aaspntest", + "location": "northcentralus", + "name": "aaspntest", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-12-06T17:18:51.88+00:00", + "lastModifiedTime": "2017-02-09T21:35:19.37+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/dsccomposite", + "location": "northcentralus", + "name": "dsccomposite", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-02-13T17:37:55.163+00:00", + "lastModifiedTime": "2017-03-06T17:19:15.09+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/dsclinux", + "location": "northcentralus", + "name": "dsclinux", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2017-03-06T21:50:05.493+00:00", + "lastModifiedTime": "2017-03-06T21:50:05.493+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/mydsc", + "location": "northcentralus", + "name": "mydsc", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-11-01T17:28:36.197+00:00", + "lastModifiedTime": "2017-02-09T21:35:19.37+00:00" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/my/providers/Microsoft.Automation/automationAccounts/mydsc2", + "location": "northcentralus", + "name": "mydsc2", + "type": "Microsoft.Automation/AutomationAccounts", + "properties": { + "creationTime": "2016-11-01T18:50:06.063+00:00", + "lastModifiedTime": "2017-03-11T01:33:13.113+00:00" + } + } + ] + } + } + } +} diff --git a/resource/testdata/examples/listRestAPIOperations.json b/resource/testdata/examples/listRestAPIOperations.json new file mode 100644 index 00000000..67d7fd70 --- /dev/null +++ b/resource/testdata/examples/listRestAPIOperations.json @@ -0,0 +1,1673 @@ +{ + "parameters": { + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "headers": {}, + "body": { + "value": [ + { + "name": "Microsoft.Automation/automationAccounts/agentRegistrationInformation/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Agent Registration", + "operation": "Gets an Azure Automation DSC's registration information", + "description": "Read an Azure Automation DSC's registration information" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/agentRegistrationInformation/regenerateKey/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Agent Registration", + "operation": "Puts a request to regenerate Azure Automation DSC keys", + "description": "Writes a request to regenerate Azure Automation DSC keys" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/operations/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Available Operations Resource", + "operation": "Reads Available Operations for Azure Automation resources", + "description": "Gets Available Operations for Azure Automation resources" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/configurations/content/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Configuration Media", + "operation": "Gets the configuration media content", + "description": "Reads the configuration media content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/deletedAutomationAccounts/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Deleted Account", + "operation": "Read an Azure Automation deleted account", + "description": "Gets an Azure Automation deleted account " + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/compilationjobs/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Compilations", + "operation": "Puts an Azure Automation DSC's Compilation", + "description": "Writes an Azure Automation DSC's Compilation" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/compilationjobs/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Compilations", + "operation": "Gets an Azure Automation DSC's Compilation", + "description": "Reads an Azure Automation DSC's Compilation" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/compilationjobs/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Compilations", + "operation": "Puts an Azure Automation DSC's Compilation", + "description": "Writes an Azure Automation DSC's Compilation" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/compilationjobs/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Compilations", + "operation": "Gets an Azure Automation DSC's Compilation", + "description": "Reads an Azure Automation DSC's Compilation" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/configurations/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Desired State Configuration", + "operation": "Read an Azure Automation DSC's content", + "description": "Gets an Azure Automation DSC's content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/configurations/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Desired State Configuration", + "operation": "Gets the count of an Azure Automation DSC's content", + "description": "Reads the count of an Azure Automation DSC's content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/configurations/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Desired State Configuration", + "operation": "Puts an Azure Automation DSC's content", + "description": "Writes an Azure Automation DSC's content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/configurations/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Desired State Configuration", + "operation": "Deletes an Azure Automation DSC's content", + "description": "Deletes an Azure Automation DSC's content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodes/reports/content/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Report Contents", + "operation": "Gets Azure Automation DSC report contents", + "description": "Reads Azure Automation DSC report contents" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodes/reports/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Reports", + "operation": "Gets Azure Automation DSC reports", + "description": "Reads Azure Automation DSC reports" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Read a Hybrid Runbook Worker Group", + "description": "Reads a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Create a Hybrid Runbook Worker Group", + "description": "Creates a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Delete a Hybrid Runbook Worker Group", + "description": "Deletes a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Read a Hybrid Runbook Worker Group", + "description": "Reads a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Create a Hybrid Runbook Worker Group", + "description": "Creates a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Group Resource Type", + "operation": "Delete a Hybrid Runbook Worker Group", + "description": "Deletes a Hybrid Runbook Worker Group" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Resource Type", + "operation": "Read a Hybrid Runbook Worker", + "description": "Reads a Hybrid Runbook Worker" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Resource Type", + "operation": "Create a Hybrid Runbook Worker", + "description": "Creates a Hybrid Runbook Worker" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers/move/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Resource Type", + "operation": "Move a Hybrid Runbook Worker from one Worker Group to another", + "description": "Moves Hybrid Runbook Worker from one Worker Group to another" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Hybrid Runbook Worker Resource Type", + "operation": "Delete a Hybrid Runbook Worker", + "description": "Deletes a Hybrid Runbook Worker" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/runbookContent/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Get runbook content", + "description": "Gets the content of the Azure Automation runbook at the time of the job execution" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/output/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Get the output of a job", + "description": "Gets the output of a job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/streams/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job Stream", + "operation": "Read an Azure Automation job stream", + "description": "Gets an Azure Automation job stream" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Read an Azure Automation job", + "description": "Gets an Azure Automation job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Create an Azure Automation job", + "description": "Creates an Azure Automation job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/stop/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Stop an Azure Automation job", + "description": "Stops an Azure Automation job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/suspend/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Suspend an Azure Automation job", + "description": "Suspends an Azure Automation job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/resume/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job", + "operation": "Resume an Azure Automation job", + "description": "Resumes an Azure Automation job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/modules/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Powershell Module", + "operation": "Read an Azure Automation Powershell module", + "description": "Gets an Azure Automation Powershell module" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/modules/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Powershell Module", + "operation": "Get the count of Powershell modules within the Automation Account", + "description": "Gets the count of Powershell modules within the Automation Account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/modules/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Powershell Module", + "operation": "Create or Update an Azure Automation Powershell module", + "description": "Creates or updates an Azure Automation Powershell module" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/modules/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Powershell Module", + "operation": "Delete an Azure Automation Powershell module", + "description": "Deletes an Azure Automation Powershell module" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connections/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Asset", + "operation": "Read an Azure Automation connection asset", + "description": "Gets an Azure Automation connection asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connections/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Asset", + "operation": "Gets the count of connections", + "description": "Reads the count of connections" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connections/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Asset", + "operation": "Create or Update an Azure Automation connection asset", + "description": "Creates or updates an Azure Automation connection asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connections/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Asset", + "operation": "Delete an Azure Automation connection asset", + "description": "Deletes an Azure Automation connection asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobSchedules/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job Schedule", + "operation": "Read an Azure Automation job schedule", + "description": "Gets an Azure Automation job schedule" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobSchedules/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job Schedule", + "operation": "Create an Azure Automation job schedule", + "description": "Creates an Azure Automation job schedule" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobSchedules/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job Schedule", + "operation": "Delete an Azure Automation job schedule", + "description": "Deletes an Azure Automation job schedule" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodeConfigurations/rawContent/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Media", + "operation": "Gets an Azure Automation DSC's node configuration content", + "description": "Reads an Azure Automation DSC's node configuration content" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodeConfigurations/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Node Configurations", + "operation": "Gets an Azure Automation DSC's node configuration", + "description": "Reads an Azure Automation DSC's node configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodeConfigurations/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Node Configurations", + "operation": "Puts an Azure Automation DSC's node configuration", + "description": "Writes an Azure Automation DSC's node configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodeConfigurations/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Node Configurations", + "operation": "Deletes an Azure Automation DSC's node configuration", + "description": "Deletes an Azure Automation DSC's node configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodecounts/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Node summary counts", + "operation": "Gets node count summary for the specified type", + "description": "Reads node count summary for the specified type" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodes/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Nodes", + "operation": "Gets Azure Automation DSC nodes", + "description": "Reads Azure Automation DSC nodes" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodes/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Nodes", + "operation": "Create or update Azure Automation DSC nodes", + "description": "Creates or updates Azure Automation DSC nodes" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/nodes/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Desired State Configuration Nodes", + "operation": "Deletes Azure Automation DSC nodes", + "description": "Deletes Azure Automation DSC nodes" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/convertGraphRunbookContent/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Convert Graph Runbook Content to its raw serialized format and vice-versa", + "description": "Convert Graph Runbook Content to its raw serialized format and vice-versa" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnections/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection", + "operation": "Azure Automation Private Endpoint Connection status", + "description": "Get Azure Automation Private Endpoint Connection status" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnections/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection", + "operation": "Azure Automation Private Endpoint Connection status", + "description": "Get Azure Automation Private Endpoint Connection status" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnections/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection", + "operation": "Approve or reject an Azure Automation Private Endpoint Connection", + "description": "Approve or reject an Azure Automation Private Endpoint Connection" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnections/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection", + "operation": "Delete an Azure Automation Private Endpoint Connection", + "description": "Delete an Azure Automation Private Endpoint Connection" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateLinkResources/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Group Information", + "operation": "Reads Group Information for private endpoints", + "description": "Reads Group Information for private endpoints" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateLinkResources/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Group Information", + "operation": "Reads Group Information for private endpoints", + "description": "Reads Group Information for private endpoints" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnectionProxies/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection Proxy", + "operation": "Reads Azure Automation Private Endpoint Connection Proxy", + "description": "Reads Azure Automation Private Endpoint Connection Proxy" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnectionProxies/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection Proxy", + "operation": "Creates an Azure Automation Private Endpoint Connection Proxy", + "description": "Creates an Azure Automation Private Endpoint Connection Proxy" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnectionProxies/validate/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection Proxy", + "operation": "Validate a Private endpoint connection request", + "description": "Validate a Private endpoint connection request (groupId Validation)" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnectionProxies/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Private Endpoint Connection Proxy", + "operation": "Delete an Azure Automation Private Endpoint Connection Proxy", + "description": "Delete an Azure Automation Private Endpoint Connection Proxy" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python2Packages/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 2 package", + "operation": "Read an Azure Automation Python 2 package", + "description": "Gets an Azure Automation Python 2 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python2Packages/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 2 package", + "operation": "Create or Update an Azure Automation Python 2 package", + "description": "Creates or updates an Azure Automation Python 2 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python2Packages/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 2 package", + "operation": "Delete an Azure Automation Python 2 package", + "description": "Deletes an Azure Automation Python 2 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python3Packages/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 3 package", + "operation": "Read an Azure Automation Python 3 package", + "description": "Gets an Azure Automation Python 3 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python3Packages/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 3 package", + "operation": "Create or Update an Azure Automation Python 3 package", + "description": "Creates or updates an Azure Automation Python 3 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/python3Packages/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Python 3 package", + "operation": "Delete an Azure Automation Python 3 package", + "description": "Deletes an Azure Automation Python 3 package" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/operationResults/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation runbook draft operation results", + "operation": "Read Azure Automation runbook draft operation results", + "description": "Gets Azure Automation runbook draft operation results" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/operationResults/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation runbook operation results", + "operation": "Read Azure Automation runbook operation results", + "description": "Gets Azure Automation runbook operation results" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/privateEndpointConnectionProxies/operationResults/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation private endpoint proxy operation results.", + "operation": "Get Azure Automation private endpoint proxy operation results.", + "description": "Get Azure Automation private endpoint proxy operation results." + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft", + "operation": "Read an Azure Automation runbook draft", + "description": "Gets an Azure Automation runbook draft" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/undoEdit/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft", + "operation": "Undo Edit to an Azure Automation runbook draft", + "description": "Undo edits to an Azure Automation runbook draft" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook", + "operation": "Read an Azure Automation Runbook", + "description": "Gets an Azure Automation runbook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook", + "operation": "Get the count of Azure Automation runbooks", + "description": "Gets the count of Azure Automation runbooks" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook", + "operation": "Create or Update an Azure Automation Runbook", + "description": "Creates or updates an Azure Automation runbook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook", + "operation": "Delete an Azure Automation Runbook", + "description": "Deletes an Azure Automation runbook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/publish/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook", + "operation": "Publish an Azure Automation runbook draft", + "description": "Publishes an Azure Automation runbook draft" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurationMachineRuns/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration Machine Run", + "operation": "Read an Azure Automation Software Update Configuration Machine Run", + "description": "Gets an Azure Automation Software Update Configuration Machine Run" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurationRuns/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration Run", + "operation": "Read an Azure Automation Software Update Configuration Run", + "description": "Gets an Azure Automation Software Update Configuration Run" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Create an Azure Automation Software Update Configuration", + "description": "Creates or updates Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Read an Azure Automation Software Update Configuration", + "description": "Gets an Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Delete an Azure Automation Software Update Configuration", + "description": "Deletes an Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/objectDataTypes/fields/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation TypeFields", + "operation": "Read Azure Automation TypeFields", + "description": "Gets Azure Automation TypeFields" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/content/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Draft Runbook Content", + "operation": "Write the content of an Azure Automation runbook draft", + "description": "Creates the content of an Azure Automation runbook draft" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/content/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Content", + "operation": "Read the content of an Azure Automation runbook", + "description": "Gets the content of an Azure Automation runbook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/schedules/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Schedule Asset", + "operation": "Read an Azure Automation schedule asset", + "description": "Gets an Azure Automation schedule asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/schedules/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Schedule Asset", + "operation": "Get the count of Azure Automation schedules", + "description": "Gets the count of Azure Automation schedules" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/schedules/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Schedule Asset", + "operation": "Create or Update an Azure Automation schedule asset", + "description": "Creates or updates an Azure Automation schedule asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/schedules/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Schedule Asset", + "operation": "Delete an Azure Automation schedule asset", + "description": "Deletes an Azure Automation schedule asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/statistics/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Statistics", + "operation": "Read Azure Automation Statistics", + "description": "Gets Azure Automation Statistics" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/testJob/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft Test Job", + "operation": "Read an Azure Automation runbook draft test job", + "description": "Gets an Azure Automation runbook draft test job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/testJob/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft Test Job", + "operation": "Create an Azure Automation runbook draft test job", + "description": "Creates an Azure Automation runbook draft test job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/testJob/stop/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft Test Job", + "operation": "Stop an Azure Automation runbook draft test job", + "description": "Stops an Azure Automation runbook draft test job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/testJob/suspend/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft Test Job", + "operation": "Suspend an Azure Automation runbook draft test job", + "description": "Suspends an Azure Automation runbook draft test job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/testJob/resume/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft Test Job", + "operation": "Resume an Azure Automation runbook draft test job", + "description": "Resumes an Azure Automation runbook draft test job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connectionTypes/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Type Asset", + "operation": "Read an Azure Automation connection asset", + "description": "Gets an Azure Automation connection type asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connectionTypes/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Type Asset", + "operation": "Create an Azure Automation connection asset", + "description": "Creates an Azure Automation connection type asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/connectionTypes/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Connection Type Asset", + "operation": "Delete an Azure Automation connection asset", + "description": "Deletes an Azure Automation connection type asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/modules/activities/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Activities", + "operation": "Read Azure Automation Activities", + "description": "Gets Azure Automation Activities" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/certificates/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Certificate Asset", + "operation": "Gets the count of certificates", + "description": "Reads the count of certificates" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/certificates/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Certificate Asset", + "operation": "Read an Azure Automation certificate", + "description": "Gets an Azure Automation certificate asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/certificates/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Certificate Asset", + "operation": "Create or Update Azure Automation certificate", + "description": "Creates or updates an Azure Automation certificate asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/certificates/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Certificate Asset", + "operation": "Delete an Azure Automation certificate", + "description": "Deletes an Azure Automation certificate asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/credentials/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Credential Asset", + "operation": "Read an Azure Automation credential asset", + "description": "Gets an Azure Automation credential asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/credentials/getCount/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Credential Asset", + "operation": "Gets the counts of credentials", + "description": "Reads the count of credentials" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/credentials/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Credential Asset", + "operation": "Create or Update an Azure Automation credential asset", + "description": "Creates or updates an Azure Automation credential asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/credentials/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Credential Asset", + "operation": "Delete an Azure Automation credential asset", + "description": "Deletes an Azure Automation credential asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/jobs/streams/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Job Stream", + "operation": "Read an Azure Automation job stream", + "description": "Gets an Azure Automation job stream" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/updateDeploymentMachineRuns/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation update deployment machine resource type", + "operation": "Get an Azure Automation update deployment machine ", + "description": "Get an Azure Automation update deployment machine" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Create an Azure Automation Software Update Configuration", + "description": "Creates or updates Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Read an Azure Automation Software Update Configuration", + "description": "Gets an Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/softwareUpdateConfigurations/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Software Update Configuration", + "operation": "Delete an Azure Automation Software Update Configuration", + "description": "Deletes an Azure Automation Software Update Configuration" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/updateManagementPatchJob/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation update management patch job resource type", + "operation": "Gets an Azure Automation update management patch job", + "description": "Gets an Azure Automation update management patch job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/linkedWorkspace/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Workspace linked to automation account", + "operation": "Read the workspace linked to the automation account", + "description": "Gets the workspace linked to the automation account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/usages/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Usage", + "operation": "Read Azure Automation Usage", + "description": "Gets Azure Automation Usage" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/watcherActions/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job Actions resource type", + "operation": "Create an Azure Automation watcher job actions", + "description": "Create an Azure Automation watcher job actions" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/watcherActions/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job Actions resource type", + "operation": "Gets an Azure Automation watcher job actions", + "description": "Gets an Azure Automation watcher job actions" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/watcherActions/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job Actions resource type", + "operation": "Delete an Azure Automation watcher job actions", + "description": "Delete an Azure Automation watcher job actions" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/streams/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher stream", + "operation": "Read an Azure Automation watcher stream", + "description": "Gets an Azure Automation watcher job stream" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job", + "operation": "Creates an Azure Automation watcher job", + "description": "Creates an Azure Automation watcher job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job", + "operation": "Gets an Azure Automation watcher job", + "description": "Gets an Azure Automation watcher job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job", + "operation": "Delete an Azure Automation watcher job", + "description": "Delete an Azure Automation watcher job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/start/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job", + "operation": "Start an Azure Automation watcher job", + "description": "Start an Azure Automation watcher job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/watchers/stop/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation watcher job", + "operation": "Stop an Azure Automation watcher job", + "description": "Stop an Azure Automation watcher job" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/webhooks/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Webhook", + "operation": "Read an Azure Automation webhook", + "description": "Reads an Azure Automation webhook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/webhooks/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Webhook", + "operation": "Create or Update an Azure Automation webhook", + "description": "Creates or updates an Azure Automation webhook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/webhooks/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Webhook", + "operation": "Delete an Azure Automation webhook", + "description": "Deletes an Azure Automation webhook " + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/webhooks/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Webhook", + "operation": "Generate a URI for an Azure Automation webhook", + "description": "Generates a URI for an Azure Automation webhook" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Read an Azure Automation account", + "description": "Gets an Azure Automation account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Create or Update an Azure Automation account", + "description": "Creates or updates an Azure Automation account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/listKeys/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Gets the Keys for the automation account", + "description": "Reads the Keys for the automation account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Delete an Azure Automation account", + "description": "Deletes an Azure Automation account" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/variables/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Variable Asset", + "operation": "Read an Azure Automation variable asset", + "description": "Reads an Azure Automation variable asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/variables/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Variable Asset", + "operation": "Create or Update an Azure Automation variable asset", + "description": "Creates or updates an Azure Automation variable asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/variables/delete", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Variable Asset", + "operation": "Delete an Azure Automation variable asset", + "description": "Deletes an Azure Automation variable asset" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/diagnosticSettings/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Get Diagnostic setting", + "description": "Gets the diagnostic setting for the resource" + }, + "origin": "system", + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/diagnosticSettings/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account", + "operation": "Set Diagnostic setting", + "description": "Sets the diagnostic setting for the resource" + }, + "origin": "system", + "properties": null + }, + { + "name": "Microsoft.Automation/automationAccounts/logDefinitions/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Account Log Definition", + "operation": "Read automation account log definitions", + "description": "Gets the available logs for the automation account" + }, + "origin": "system", + "properties": { + "serviceSpecification": { + "metricSpecifications": null, + "logSpecifications": [ + { + "name": "JobLogs", + "displayName": "Job Logs", + "blobDuration": "PT1H" + }, + { + "name": "JobStreams", + "displayName": "Job Streams", + "blobDuration": "PT1H" + }, + { + "name": "DscNodeStatus", + "displayName": "Dsc Node Status", + "blobDuration": "PT1H" + } + ] + } + } + }, + { + "name": "Microsoft.Automation/automationAccounts/providers/Microsoft.Insights/metricDefinitions/read", + "display": { + "provider": "Microsoft Automation", + "resource": "Automation Metric Definitions", + "operation": "Read Automation Metric Definitions", + "description": "Gets Automation Metric Definitions" + }, + "origin": "system", + "properties": { + "serviceSpecification": { + "metricSpecifications": [ + { + "name": "TotalJob", + "displayName": "Total Jobs", + "displayDescription": "The total number of jobs", + "unit": "Count", + "aggregationType": "Total", + "dimensions": [ + { + "name": "Runbook", + "displayName": "Runbook Name" + }, + { + "name": "Status", + "displayName": "Status" + } + ] + }, + { + "name": "TotalUpdateDeploymentRuns", + "displayName": "Total Update Deployment Runs", + "displayDescription": "Total software update deployment runs", + "unit": "Count", + "aggregationType": "Total", + "dimensions": [ + { + "name": "SoftwareUpdateConfigurationName", + "displayName": "Update Deployment Name" + }, + { + "name": "Status", + "displayName": "Status" + } + ] + }, + { + "name": "TotalUpdateDeploymentMachineRuns", + "displayName": "Total Update Deployment Machine Runs", + "displayDescription": "Total software update deployment machine runs in a software update deployment run", + "unit": "Count", + "aggregationType": "Total", + "dimensions": [ + { + "name": "SoftwareUpdateConfigurationName", + "displayName": "Update Deployment Name" + }, + { + "name": "Status", + "displayName": "Status" + }, + { + "name": "TargetComputer", + "displayName": "Target Computer" + }, + { + "name": "SoftwareUpdateConfigurationRunId", + "displayName": "Update Deployment Run Id" + } + ] + } + ], + "logSpecifications": null + } + } + }, + { + "name": "Microsoft.Automation/automationAccounts/runbooks/draft/write", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation Runbook Draft", + "operation": "Write an Azure Automation runbook draft", + "description": "Creates an Azure Automation runbook draft" + }, + "origin": null, + "properties": null + }, + { + "name": "Microsoft.Automation/register/action", + "display": { + "provider": "Microsoft Automation", + "resource": "Azure Automation subscription resource type", + "operation": "Register the subscription to Azure Automation", + "description": "Registers the subscription to Azure Automation" + }, + "origin": null, + "properties": null + } + ] + } + } + } +} diff --git a/resource/testdata/examples/sourceControl/createOrUpdateSourceControl.json b/resource/testdata/examples/sourceControl/createOrUpdateSourceControl.json new file mode 100644 index 00000000..344a7be2 --- /dev/null +++ b/resource/testdata/examples/sourceControl/createOrUpdateSourceControl.json @@ -0,0 +1,62 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "sampleAccount9", + "sourceControlName": "sampleSourceControl", + "api-version": "2022-08-08", + "parameters": { + "properties": { + "repoUrl": "https://sampleUser.visualstudio.com/myProject/_git/myRepository", + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "sourceType": "VsoGit", + "securityToken": { + "accessToken": "******", + "tokenType": "PersonalAccessToken" + }, + "description": "my description" + } + } + }, + "responses": { + "201": { + "headers": {}, + "body": { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl", + "name": "sampleSourceControl", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://sampleUser.visualstudio.com/myProject/_git/myRepository", + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "sourceType": "VsoGit", + "description": "my description" + } + } + }, + "200": { + "headers": {}, + "body": { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl", + "name": "sampleSourceControl", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://sampleUser.visualstudio.com/myProject/_git/myRepository", + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "sourceType": "VsoGit", + "description": "my description" + } + } + } + } +} diff --git a/resource/testdata/examples/sourceControl/deleteSourceControl.json b/resource/testdata/examples/sourceControl/deleteSourceControl.json new file mode 100644 index 00000000..30390d1a --- /dev/null +++ b/resource/testdata/examples/sourceControl/deleteSourceControl.json @@ -0,0 +1,12 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "sampleAccount9", + "sourceControlName": "sampleSourceControl", + "api-version": "2022-08-08" + }, + "responses": { + "200": {} + } +} diff --git a/resource/testdata/examples/sourceControl/getAllSourceControls.json b/resource/testdata/examples/sourceControl/getAllSourceControls.json new file mode 100644 index 00000000..e3005b4b --- /dev/null +++ b/resource/testdata/examples/sourceControl/getAllSourceControls.json @@ -0,0 +1,91 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "sampleAccount9", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "body": { + "value": [ + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl1", + "name": "sampleSourceControl1", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell-1", + "branch": "master", + "folderPath": "/sampleFolder/sampleFolder2", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl2", + "name": "sampleSourceControl2", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell-2", + "branch": "master", + "folderPath": "/sampleFolder/sampleFolder2", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl3", + "name": "sampleSourceControl3", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell-3", + "branch": "master", + "folderPath": "/sampleFolder/sampleFolder2", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl4", + "name": "sampleSourceControl4", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell-4", + "branch": "master", + "folderPath": "/sampleFolder/sampleFolder2", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl5", + "name": "sampleSourceControl5", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell-5", + "branch": "master", + "folderPath": "/sampleFolder/sampleFolder2", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + } + ] + } + } + } +} diff --git a/resource/testdata/examples/sourceControl/getSourceControl.json b/resource/testdata/examples/sourceControl/getSourceControl.json new file mode 100644 index 00000000..cf681710 --- /dev/null +++ b/resource/testdata/examples/sourceControl/getSourceControl.json @@ -0,0 +1,28 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "sampleAccount9", + "sourceControlName": "sampleSourceControl", + "api-version": "2022-08-08" + }, + "responses": { + "200": { + "body": { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl", + "name": "sampleSourceControl", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://github.com/SampleUserRepro/PowerShell", + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "sourceType": "GitHub", + "description": "my description" + } + } + } + } +} diff --git a/resource/testdata/examples/sourceControl/updateSourceControl_patch.json b/resource/testdata/examples/sourceControl/updateSourceControl_patch.json new file mode 100644 index 00000000..8d3cec55 --- /dev/null +++ b/resource/testdata/examples/sourceControl/updateSourceControl_patch.json @@ -0,0 +1,42 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "sampleAccount9", + "sourceControlName": "sampleSourceControl", + "api-version": "2022-08-08", + "parameters": { + "properties": { + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "securityToken": { + "accessToken": "******", + "tokenType": "PersonalAccessToken" + }, + "description": "my description" + } + } + }, + "responses": { + "200": { + "headers": {}, + "body": { + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/sampleAccount9/sourcecontrols/sampleSourceControl", + "name": "sampleSourceControl", + "properties": { + "creationTime": "2017-03-28T22:59:00.937+00:00", + "lastModifiedTime": "2017-03-28T22:59:00.937+00:00", + "repoUrl": "https://sampleUser.visualstudio.com/myProject/_git/myRepository", + "branch": "master", + "folderPath": "/folderOne/folderTwo", + "autoSync": true, + "publishRunbook": true, + "sourceType": "VsoGit", + "description": "my description" + } + } + } + } +} diff --git a/resource/testdata/examples/updateAutomationAccount.json b/resource/testdata/examples/updateAutomationAccount.json new file mode 100644 index 00000000..bd9ab3a1 --- /dev/null +++ b/resource/testdata/examples/updateAutomationAccount.json @@ -0,0 +1,41 @@ +{ + "parameters": { + "subscriptionId": "subid", + "resourceGroupName": "rg", + "automationAccountName": "myAutomationAccount9", + "api-version": "2022-08-08", + "parameters": { + "properties": { + "sku": { + "name": "Free" + } + }, + "name": "myAutomationAccount9", + "location": "East US 2" + } + }, + "responses": { + "200": { + "headers": {}, + "body": { + "name": "myAutomationAccount9", + "id": "/subscriptions/subid/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/myAutomationAccount9", + "type": "Microsoft.Automation/AutomationAccounts", + "location": "East US 2", + "tags": {}, + "etag": null, + "properties": { + "sku": { + "name": "Free", + "family": null, + "capacity": null + }, + "state": "Ok", + "creationTime": "2017-03-26T01:13:43.267+00:00", + "lastModifiedBy": "myEmailId@microsoft.com", + "lastModifiedTime": "2017-03-26T01:13:43.267+00:00" + } + } + } + } +} diff --git a/resource/types/azapi_definition.go b/resource/types/azapi_definition.go index 32a36431..95704f09 100644 --- a/resource/types/azapi_definition.go +++ b/resource/types/azapi_definition.go @@ -1,9 +1,8 @@ -package resource +package types import ( + "encoding/json" "fmt" - "github.com/ms-henglu/armstrong/resource/types" - "github.com/ms-henglu/armstrong/hcl" ) @@ -15,9 +14,17 @@ type AzapiDefinition struct { AzureResourceType string // example: Microsoft.Network/virtualNetworks ApiVersion string // example: 2020-06-01 Body interface{} - AdditionalFields map[string]types.Value // fields like resource_id, parent_id, name, location, action, method + AdditionalFields map[string]Value // fields like resource_id, parent_id, name, location, action, method + BodyFormat BodyFormat // hcl or json } +type BodyFormat string + +const ( + BodyFormatHcl BodyFormat = "hcl" + BodyFormatJson BodyFormat = "json" +) + type Kind string const ( @@ -45,11 +52,18 @@ func (def AzapiDefinition) String() string { } } if len(bodyMap) > 0 { - expressions += fmt.Sprintf(` + if def.BodyFormat == BodyFormatJson { + jsonBody, _ := json.MarshalIndent(bodyMap, "", " ") + expressions += fmt.Sprintf(`< 0 { + exampleMap[method] = exampleList[0] + } + } + + sort.Strings(methods) + apiPath.Methods = methods + apiPath.ExampleMap = exampleMap + + apiPaths = append(apiPaths, apiPath) + } + + // check the operation whether it's a list operation + // for example: + // /subscriptions/{subscriptionId}/providers/Microsoft.Automation/automationAccounts + // /.../providers/Microsoft.Automation/automationAccounts/{automationAccountName}/modules + resourceTypes := make(map[string]bool) + // add a special resource type "operations" + resourceTypes["operations"] = true + for _, apiPath := range apiPaths { + if lastSegment := utils.LastSegment(apiPath.ResourceType); lastSegment != "" { + resourceTypes[lastSegment] = true + } + } + for i, apiPath := range apiPaths { + if len(apiPath.Methods) != 1 || apiPath.Methods[0] != http.MethodGet { + continue + } + lastSegment := utils.LastSegment(apiPath.Path) + if lastSegment != "" && resourceTypes[lastSegment] { + apiPaths[i].ResourceType += "/" + lastSegment + apiPaths[i].ApiType = ApiTypeList + } + } + + // decide the api type + for i, apiPath := range apiPaths { + if apiPath.ApiType != ApiTypeUnknown { + continue + } + switch { + case !utils.IsAction(apiPath.Path): + apiPaths[i].ApiType = ApiTypeResource + case strings.Contains(apiPath.ResourceType, "/"): + apiPaths[i].ApiType = ApiTypeResourceAction + default: + apiPaths[i].ApiType = ApiTypeProviderAction + } + } + + slices.SortFunc(apiPaths, func(i, j ApiPath) int { + return strings.Compare(i.Path, j.Path) + }) + return apiPaths, nil +} diff --git a/swagger/swagger_test.go b/swagger/swagger_test.go index c0a891a2..98297604 100644 --- a/swagger/swagger_test.go +++ b/swagger/swagger_test.go @@ -1 +1,186 @@ -package swagger +package swagger_test + +import ( + "log" + "os" + "path" + "testing" + + "github.com/ms-henglu/armstrong/swagger" +) + +func Test_LoadSwagger(t *testing.T) { + wd, _ := os.Getwd() + testcases := []struct { + Input string + ApiPaths []swagger.ApiPath + ExpectError bool + }{ + { + Input: path.Clean(path.Join(wd, "testdata", "account.json")), + ApiPaths: []swagger.ApiPath{ + { + Path: "/subscriptions/{subscriptionId}/providers/Microsoft.Automation/automationAccounts", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeList, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/listAutomationAccountsBySubscription.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeList, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/listAutomationAccountsByResourceGroup.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeResource, + Methods: []string{"DELETE", "GET", "PATCH", "PUT"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/getAutomationAccount.json")), + "PATCH": path.Clean(path.Join(wd, "testdata", "./examples/updateAutomationAccount.json")), + "PUT": path.Clean(path.Join(wd, "testdata", "./examples/createOrUpdateAutomationAccount.json")), + "DELETE": path.Clean(path.Join(wd, "testdata", "./examples/deleteAutomationAccount.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/listKeys", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeResourceAction, + Methods: []string{"POST"}, + ExampleMap: map[string]string{ + "POST": path.Clean(path.Join(wd, "testdata", "./examples/listAutomationAccountKeys.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/statistics", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeResourceAction, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/getStatisticsOfAutomationAccount.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/usages", + ResourceType: "Microsoft.Automation/automationAccounts", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeResourceAction, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/getUsagesOfAutomationAccount.json")), + }, + }, + }, + ExpectError: false, + }, + { + Input: path.Clean(path.Join(wd, "testdata", "sourceControl.json")), + ApiPaths: []swagger.ApiPath{ + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/sourceControls", + ResourceType: "Microsoft.Automation/automationAccounts/sourceControls", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeList, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/sourceControl/getAllSourceControls.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/sourceControls/{sourceControlName}", + ResourceType: "Microsoft.Automation/automationAccounts/sourceControls", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeList, + Methods: []string{"DELETE", "GET", "PATCH", "PUT"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/sourceControl/getSourceControl.json")), + "PATCH": path.Clean(path.Join(wd, "testdata", "./examples/sourceControl/updateSourceControl_patch.json")), + "PUT": path.Clean(path.Join(wd, "testdata", "./examples/sourceControl/createOrUpdateSourceControl.json")), + "DELETE": path.Clean(path.Join(wd, "testdata", "./examples/sourceControl/deleteSourceControl.json")), + }, + }, + }, + ExpectError: false, + }, + { + Input: path.Clean(path.Join(wd, "testdata", "operations.json")), + ApiPaths: []swagger.ApiPath{ + { + Path: "/providers/Microsoft.Automation/operations", + ResourceType: "Microsoft.Automation/operations", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeList, + Methods: []string{"GET"}, + ExampleMap: map[string]string{ + "GET": path.Clean(path.Join(wd, "testdata", "./examples/listRestAPIOperations.json")), + }, + }, + { + Path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/convertGraphRunbookContent", + ResourceType: "Microsoft.Automation", + ApiVersion: "2022-08-08", + ApiType: swagger.ApiTypeProviderAction, + Methods: []string{"POST"}, + ExampleMap: map[string]string{ + "POST": path.Clean(path.Join(wd, "testdata", "./examples/deserializeGraphRunbookContent.json")), + }, + }, + }, + ExpectError: false, + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] testcase: %s", testcase.Input) + apiPaths, err := swagger.Load(testcase.Input) + if err != nil && !testcase.ExpectError { + t.Errorf("unexpected error: %+v", err) + } + if err == nil && testcase.ExpectError { + t.Errorf("expected error but got nil") + } + if len(apiPaths) != len(testcase.ApiPaths) { + t.Errorf("expected %d api paths but got %d", len(testcase.ApiPaths), len(apiPaths)) + } + for i, apiPath := range apiPaths { + log.Printf("[DEBUG] api path: %+v", testcase.ApiPaths[i].Path) + if apiPath.Path != testcase.ApiPaths[i].Path { + t.Errorf("expected api path %s but got %s", testcase.ApiPaths[i].Path, apiPath.Path) + } + if apiPath.ResourceType != testcase.ApiPaths[i].ResourceType { + t.Errorf("expected resource type %s but got %s", testcase.ApiPaths[i].ResourceType, apiPath.ResourceType) + } + if apiPath.ApiVersion != testcase.ApiPaths[i].ApiVersion { + t.Errorf("expected api version %s but got %s", testcase.ApiPaths[i].ApiVersion, apiPath.ApiVersion) + } + if len(apiPath.Methods) != len(testcase.ApiPaths[i].Methods) { + t.Errorf("expected %d methods but got %d", len(testcase.ApiPaths[i].Methods), len(apiPath.Methods)) + } + for j, method := range apiPath.Methods { + if method != testcase.ApiPaths[i].Methods[j] { + t.Errorf("expected method %s but got %s", testcase.ApiPaths[i].Methods[j], method) + } + } + if len(apiPath.ExampleMap) != len(testcase.ApiPaths[i].ExampleMap) { + t.Errorf("expected %d examples but got %d", len(testcase.ApiPaths[i].ExampleMap), len(apiPath.ExampleMap)) + } + for method, example := range apiPath.ExampleMap { + if example != testcase.ApiPaths[i].ExampleMap[method] { + t.Errorf("expected example %s but got %s", testcase.ApiPaths[i].ExampleMap[method], example) + } + } + } + } +} diff --git a/swagger/testdata/operations.json b/swagger/testdata/operations.json index 3e40df49..4d46acda 100644 --- a/swagger/testdata/operations.json +++ b/swagger/testdata/operations.json @@ -72,7 +72,7 @@ } } }, - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/automationAccounts/{automationAccountName}/convertGraphRunbookContent": { + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Automation/convertGraphRunbookContent": { "post": { "tags": [ "Operations" diff --git a/swagger/types.go b/swagger/types.go index e69de29b..30916404 100644 --- a/swagger/types.go +++ b/swagger/types.go @@ -0,0 +1,20 @@ +package swagger + +type ApiPath struct { + Path string + ResourceType string + ApiVersion string + ExampleMap map[string]string + Methods []string + ApiType ApiType +} + +type ApiType string + +const ( + ApiTypeUnknown ApiType = "unknown" + ApiTypeList ApiType = "list" + ApiTypeResource ApiType = "resource" + ApiTypeResourceAction ApiType = "resourceAction" + ApiTypeProviderAction ApiType = "providerAction" +) diff --git a/tf/terraform.go b/tf/terraform.go index 6ceaff08..9eb4a5c7 100644 --- a/tf/terraform.go +++ b/tf/terraform.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" tfjson "github.com/hashicorp/terraform-json" + "github.com/sirupsen/logrus" ) type Terraform struct { @@ -46,7 +47,7 @@ func (t *Terraform) SetLogEnabled(enabled bool) { if enabled && t.LogEnabled { t.exec.SetStdout(os.Stdout) t.exec.SetStderr(os.Stderr) - t.exec.SetLogger(log.New(os.Stdout, "", 0)) + t.exec.SetLogger(logrus.StandardLogger()) } else { t.exec.SetStdout(io.Discard) t.exec.SetStderr(io.Discard) @@ -58,7 +59,7 @@ func (t *Terraform) Init() error { if _, err := os.Stat(".terraform"); os.IsNotExist(err) { return t.exec.Init(context.Background(), tfexec.Upgrade(false)) } - log.Println("[INFO] skip running init command because .terraform folder exists") + logrus.Infof("skip running init command because .terraform folder exists") return nil } diff --git a/tf/utils.go b/tf/utils.go index 440f065f..c3b711b7 100644 --- a/tf/utils.go +++ b/tf/utils.go @@ -3,14 +3,14 @@ package tf import ( "encoding/json" "fmt" - "log" "regexp" "strings" tfjson "github.com/hashicorp/terraform-json" "github.com/ms-henglu/armstrong/coverage" - "github.com/ms-henglu/armstrong/resource/utils" "github.com/ms-henglu/armstrong/types" + "github.com/ms-henglu/armstrong/utils" + "github.com/sirupsen/logrus" ) type Action string @@ -97,7 +97,7 @@ func NewPassReportFromState(state *tfjson.State) types.PassReport { Resources: make([]types.Resource, 0), } if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil { - log.Printf("[WARN] new pass report from state: state is nil") + logrus.Warnf("new pass report from state: state is nil") return out } for _, res := range state.Values.RootModule.Resources { @@ -149,7 +149,7 @@ func NewPassReport(plan *tfjson.Plan) types.PassReport { func NewCoverageReportFromState(state *tfjson.State) (coverage.CoverageReport, error) { defer func() { if r := recover(); r != nil { - log.Printf("[ERROR] panic when producing coverage report from state: %+v", r) + logrus.Errorf("panic when producing coverage report from state: %+v", r) } }() @@ -157,7 +157,7 @@ func NewCoverageReportFromState(state *tfjson.State) (coverage.CoverageReport, e Coverages: make(map[coverage.ArmResource]*coverage.Model, 0), } if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil { - log.Print("[WARN] new coverage report from state: state is nil") + logrus.Warnf("new coverage report from state: state is nil") return out, nil } for _, res := range state.Values.RootModule.Resources { @@ -190,7 +190,7 @@ func NewCoverageReportFromState(state *tfjson.State) (coverage.CoverageReport, e func NewCoverageReport(plan *tfjson.Plan) (coverage.CoverageReport, error) { defer func() { if r := recover(); r != nil { - log.Printf("[ERROR] panic when producing coverage report: %+v", r) + logrus.Errorf("panic when producing coverage report: %+v", r) } }() @@ -311,7 +311,7 @@ func NewErrorReport(applyErr error, logs []types.RequestTrace) types.ErrorReport } out.Errors = append(out.Errors, types.Error{ Id: id, - Type: fmt.Sprintf("%s@%s", utils.GetResourceType(id), apiVersion), + Type: fmt.Sprintf("%s@%s", utils.ResourceTypeOfResourceId(id), apiVersion), Label: label, Message: errorMessage, }) @@ -340,7 +340,7 @@ func NewCleanupErrorReport(applyErr error, logs []types.RequestTrace) types.Erro out.Errors = append(out.Errors, types.Error{ Id: id, - Type: fmt.Sprintf("%s@%s", utils.GetResourceType(id), apiVersion), + Type: fmt.Sprintf("%s@%s", utils.ResourceTypeOfResourceId(id), apiVersion), Message: errorMessage, }) } @@ -350,7 +350,7 @@ func NewCleanupErrorReport(applyErr error, logs []types.RequestTrace) types.Erro func NewIdAdressFromState(state *tfjson.State) map[string]string { out := map[string]string{} if state == nil || state.Values == nil || state.Values.RootModule == nil || state.Values.RootModule.Resources == nil { - log.Printf("[WARN] new id address mapping from state: state is nil") + logrus.Warnf("new id address mapping from state: state is nil") return out } for _, res := range state.Values.RootModule.Resources { diff --git a/utils/body.go b/utils/body.go index d4b585bf..569cac04 100644 --- a/utils/body.go +++ b/utils/body.go @@ -1 +1,42 @@ package utils + +import ( + "fmt" + "strconv" +) + +func UpdatedBody(body interface{}, replacements map[string]string, path string) interface{} { + if len(replacements) == 0 { + return body + } + switch bodyValue := body.(type) { + case map[string]interface{}: + res := make(map[string]interface{}) + for key, value := range bodyValue { + if temp := UpdatedBody(value, replacements, path+"."+key); temp != nil { + if replaceKey := replacements[fmt.Sprintf("key:%s.%s", path, key)]; replaceKey != "" { + key = replaceKey + } + res[key] = temp + } + } + return res + case []interface{}: + res := make([]interface{}, 0) + for index, value := range bodyValue { + if temp := UpdatedBody(value, replacements, path+"."+strconv.Itoa(index)); temp != nil { + res = append(res, temp) + } + } + return res + case string: + for key, replacement := range replacements { + if key == path { + return replacement + } + } + default: + + } + return body +} diff --git a/utils/body_test.go b/utils/body_test.go new file mode 100644 index 00000000..20d50dad --- /dev/null +++ b/utils/body_test.go @@ -0,0 +1,38 @@ +package utils_test + +import ( + "encoding/json" + "github.com/ms-henglu/armstrong/utils" + "testing" +) + +const inputJson = ` +{ + "location": "eastus", + "properties": { + "computeType": "ComputeInstance", + "properties": { + "vmSize": "STANDARD_NC6", + "subnet": "test-subnet-resource-id", + "applicationSharingPolicy": "Personal", + "sshSettings": { + "sshPublicAccess": "Disabled" + } + } + } + }` + +func Test_UpdatedBody(t *testing.T) { + var parameter interface{} + _ = json.Unmarshal([]byte(inputJson), ¶meter) + + replacements := make(map[string]string) + subnetId := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/virtualNetworks/myvnet1/subnets/mysubnet1" + replacements[".properties.properties.subnet"] = subnetId + output := utils.UpdatedBody(parameter, replacements, "").(map[string]interface{}) + output = output["properties"].(map[string]interface{}) + output = output["properties"].(map[string]interface{}) + if output["subnet"].(string) != subnetId { + t.Fatalf("expect %s but got %v", subnetId, output["subnet"]) + } +} diff --git a/utils/hcl.go b/utils/hcl.go index d4b585bf..30ac4e66 100644 --- a/utils/hcl.go +++ b/utils/hcl.go @@ -1 +1,21 @@ package utils + +import ( + "strings" + + "github.com/hashicorp/hcl/v2/hclwrite" +) + +func TypeValue(block *hclwrite.Block) string { + typeAttribute := block.Body().GetAttribute("type") + return AttributeValue(typeAttribute) +} + +func AttributeValue(attribute *hclwrite.Attribute) string { + if attribute == nil { + return "" + } + value := string(attribute.Expr().BuildTokens(nil).Bytes()) + value = strings.Trim(value, ` "`) + return value +} diff --git a/utils/hcl_test.go b/utils/hcl_test.go new file mode 100644 index 00000000..f8d15564 --- /dev/null +++ b/utils/hcl_test.go @@ -0,0 +1,151 @@ +package utils_test + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/ms-henglu/armstrong/utils" +) + +func Test_TypeValue(t *testing.T) { + testcases := []struct { + Input string + Expected []string + }{ + { + Input: ` +terraform { + required_providers { + azapi = { + source = "Azure/azapi" + } + } +} + +provider "azapi" { + skip_provider_registration = false +} + +variable "resource_name" { + type = string + default = "acctest0001" +} + +variable "location" { + type = string + default = "westeurope" +} + +resource "azapi_resource" "resourceGroup" { + type = "Microsoft.Resources/resourceGroups@2020-06-01" + name = var.resource_name + location = var.location +} + +resource "azapi_resource" "Spring" { + type = "Microsoft.AppPlatform/Spring@2023-05-01-preview" + parent_id = azapi_resource.resourceGroup.id + name = var.resource_name + location = var.location + body = jsonencode({ + properties = { + zoneRedundant = false + } + sku = { + name = "E0" + } + }) + schema_validation_enabled = false + response_export_values = ["*"] +} + +resource "azapi_resource" "app" { + type = "Microsoft.AppPlatform/Spring/apps@2023-05-01-preview" + parent_id = azapi_resource.Spring.id + name = var.resource_name + location = var.location + body = jsonencode({ + identity = { + type = "None" + } + properties = { + customPersistentDisks = [ + ] + enableEndToEndTLS = false + public = false + } + }) + schema_validation_enabled = false + response_export_values = ["*"] +} + +resource "azapi_resource" "domain" { + type = "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01" + parent_id = azapi_resource.app.id + name = var.resource_name + body = jsonencode({ + properties = { + certName = "mycert" + thumbprint = "934367bf1c97033f877db0f15cb1b586957d3133" + } + }) + schema_validation_enabled = false +} + +resource "azapi_resource_action" "patch_domain" { + type = "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01" + resource_id = azapi_resource.domain.id + action = "" + method = "PATCH" + body = jsonencode({ + properties = { + certName = "mycert" + thumbprint = "934367bf1c97033f877db0f15cb1b586957d3133" + } + }) +} + +data "azapi_resource_list" "listDomainsByApp" { + type = "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01" + parent_id = azapi_resource.app.id + depends_on = [azapi_resource.domain] +} + +`, + Expected: []string{ + "", + "", + "", + "", + "Microsoft.Resources/resourceGruops@2020-06-1", + "Microsoft.AppPlatform/Spring@2023-05-01-preview", + "Microsoft.AppPlatform/Spring/apps@2023-05-01-preview", + "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01", + "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01", + "Microsoft.AppPlatform/Spring/apps/domains@2022-04-01", + }, + }, + } + + for _, tc := range testcases { + file, diags := hclwrite.ParseConfig([]byte(tc.Input), "", hcl.InitialPos) + if diags.HasErrors() { + t.Error(diags) + } + actuals := make([]string, 0) + for _, block := range file.Body().Blocks() { + actual := utils.TypeValue(block) + actuals = append(actuals, actual) + } + if len(actuals) != len(tc.Expected) { + t.Errorf("expected %d, got %d", len(tc.Expected), len(actuals)) + continue + } + for i, actual := range actuals { + if actual != tc.Expected[i] { + t.Errorf("expected %s, got %s", tc.Expected[i], actual) + } + } + } +} diff --git a/utils/id.go b/utils/id.go index d4b585bf..22570a19 100644 --- a/utils/id.go +++ b/utils/id.go @@ -1 +1,119 @@ package utils + +import ( + "fmt" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/sirupsen/logrus" +) + +func IsResourceId(input string) bool { + id := strings.Trim(input, "/") + if len(strings.Split(id, "/"))%2 == 1 { + return false + } + _, err := arm.ParseResourceID(input) + return err == nil +} + +func ActionName(input string) string { + if !IsAction(input) { + return "" + } + return LastSegment(input) +} + +func LastSegment(input string) string { + id := strings.Trim(input, "/") + components := strings.Split(id, "/") + if len(components) == 0 { + return "" + } + return components[len(components)-1] +} + +func IsAction(input string) bool { + id := strings.Trim(input, "/") + components := strings.Split(id, "/") + return len(components)%2 == 1 +} + +func ResourceIdOfAction(input string) string { + id := strings.Trim(input, "/") + components := strings.Split(id, "/") + return fmt.Sprintf("/%s", strings.Join(components[:len(components)-1], "/")) +} + +func ScopeOfListAction(input string) string { + id := fmt.Sprintf("%s/{placeholder}", input) + return ParentIdOfResourceId(id) +} + +func ParentIdOfResourceId(input string) string { + resourceId, err := arm.ParseResourceID(input) + if err == nil && resourceId.Parent != nil { + if resourceId.Parent.ResourceType.String() == arm.TenantResourceType.String() { + return "/" + } + return resourceId.Parent.String() + } + if !strings.Contains(ResourceTypeOfResourceId(input), "/") { + id := strings.Trim(input, "/") + components := strings.Split(id, "/") + if len(components) <= 2 { + logrus.Warnf("no parent_id found for resource id: %s", input) + return "" + } + return fmt.Sprintf("/%s", strings.Join(components[:len(components)-2], "/")) + } + return "" +} + +func ResourceTypeOfResourceId(input string) string { + if input == "/" { + return arm.TenantResourceType.String() + } + id := input + if IsAction(id) { + id = ResourceIdOfAction(id) + } + if resourceType, err := arm.ParseResourceType(id); err == nil { + if resourceType.Type != arm.ProviderResourceType.Type { + return resourceType.String() + } + } + + idURL, err := url.ParseRequestURI(id) + if err != nil { + return "" + } + + path := idURL.Path + + path = strings.TrimPrefix(path, "/") + path = strings.TrimSuffix(path, "/") + + components := strings.Split(path, "/") + + resourceType := "" + provider := "" + for current := 0; current < len(components)-1; current += 2 { + key := components[current] + value := components[current+1] + + // Check key/value for empty strings. + if key == "" || value == "" { + return "" + } + + if key == "providers" { + provider = value + resourceType = provider + } else if len(provider) > 0 { + resourceType += "/" + key + } + } + return resourceType +} diff --git a/utils/id_test.go b/utils/id_test.go index d4b585bf..acd052cf 100644 --- a/utils/id_test.go +++ b/utils/id_test.go @@ -1 +1,162 @@ -package utils +package utils_test + +import ( + "log" + "testing" + + "github.com/ms-henglu/armstrong/utils" +) + +func Test_IsResourceId(t *testing.T) { + testcases := []struct { + Input string + Expect bool + }{ + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expect: true, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg", + Expect: true, + }, + { + Input: "/", + Expect: false, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb/providers/Microsoft.Resources/locks/myLock", + Expect: true, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb", + Expect: true, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb/providers/Microsoft.Resources/locks/myLock/foo", + Expect: false, + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb/foo", + Expect: false, + }, + } + + for _, testcase := range testcases { + t.Logf("[DEBUG] testcase: %s", testcase.Input) + actual := utils.IsResourceId(testcase.Input) + if actual != testcase.Expect { + t.Fatalf("[ERROR] expect %v, actual %v", testcase.Expect, actual) + } + } +} + +func Test_ResourceTypeOfResourceId(t *testing.T) { + testcases := []struct { + Input string + Expect string + }{ + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expect: "Microsoft.Resources/subscriptions", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg", + Expect: "Microsoft.Resources/resourceGroups", + }, + { + Input: "/", + Expect: "Microsoft.Resources/tenants", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb/providers/Microsoft.Resources/locks/myLock", + Expect: "Microsoft.Resources/locks", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb", + Expect: "Microsoft.Automation/automationAccounts/runbooks", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa", + Expect: "Microsoft.Automation/automationAccounts", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts", + // though it would be better to return Microsoft.Automation/automationAccounts, but there's no way to know it's a list API or a provider action, see below case + Expect: "Microsoft.Automation", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/checkNameAvailability", + Expect: "Microsoft.Automation", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Automation/automationAccounts", + // though it would be better to return Microsoft.Automation/automationAccounts, but there's no way to know it's a list API or a provider action, see below case + Expect: "Microsoft.Automation", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Automation/checkNameAvailability", + Expect: "Microsoft.Automation", + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] testcase: %s", testcase.Input) + actual := utils.ResourceTypeOfResourceId(testcase.Input) + if actual != testcase.Expect { + t.Fatalf("[ERROR] expect %s, actual %s", testcase.Expect, actual) + } + } +} + +func Test_PrentIdOfResourceId(t *testing.T) { + testcases := []struct { + Input string + Expect string + }{ + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expect: "/", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000", + }, + { + Input: "/", + Expect: "", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb/providers/Microsoft.Resources/locks/myLock", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa/runbooks/rb", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation/automationAccounts/aa", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Automation", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg", + }, + { + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Automation", + Expect: "/subscriptions/00000000-0000-0000-0000-000000000000", + }, + { + Input: "/providers/Microsoft.Automation", + Expect: "/", + }, + } + + for _, testcase := range testcases { + log.Printf("[DEBUG] testcase: %s", testcase.Input) + actual := utils.ParentIdOfResourceId(testcase.Input) + if actual != testcase.Expect { + t.Fatalf("[ERROR] expect %s, actual %s", testcase.Expect, actual) + } + } +} diff --git a/version.go b/version.go index e7656f84..bf76f826 100644 --- a/version.go +++ b/version.go @@ -1,5 +1,5 @@ package main func VersionString() string { - return "0.11.0" + return "0.12.0" }