diff --git a/cmd/infracost/breakdown_test.go b/cmd/infracost/breakdown_test.go index b7b6b0f994c..d13a3cb0119 100644 --- a/cmd/infracost/breakdown_test.go +++ b/cmd/infracost/breakdown_test.go @@ -238,7 +238,21 @@ func TestBreakdownConfigFile(t *testing.T) { }, ) } - +func TestBreakdownArmTemplateConfigFile(t *testing.T) { + testName := testutil.CalcGoldenFileTestdataDirName() + dir := path.Join("./testdata", testName) + GoldenFileCommandTest( + t, + testutil.CalcGoldenFileTestdataDirName(), + []string{ + "breakdown", + "--config-file", path.Join(dir, "infracost.yml"), + }, + &GoldenFileOptions{ + IsJSON: true, + }, + ) +} func TestBreakdownConfigFileWithUsageFile(t *testing.T) { testName := testutil.CalcGoldenFileTestdataDirName() dir := path.Join("./testdata", testName) diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden b/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden new file mode 100644 index 00000000000..11ae6be8b57 --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/breakdown_arm_template_config_file.golden @@ -0,0 +1,23 @@ +Project: standard_disk + + Name Monthly Qty Unit Monthly Cost + + Microsoft.Compute/disks/standard + ├─ Storage (S40, LRS) 1 months $85.60 + └─ Disk operations Monthly cost depends on usage: $0.0005 per 10k operations + + OVERALL TOTAL $85.60 + +*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options. + +────────────────────────────────── +No cloud resources were detected + +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ +┃ Project ┃ Baseline cost ┃ Usage cost* ┃ Total cost ┃ +┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━┫ +┃ standard_disk ┃ $86 ┃ - ┃ $86 ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━┛ + +Err: + diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml b/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml new file mode 100644 index 00000000000..374a9add0d9 --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/infracost.yml @@ -0,0 +1,4 @@ +version: 0.1 +projects: + - path: ./testdata/breakdown_arm_template_config_file/standard_disk.json + name : standard_disk \ No newline at end of file diff --git a/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json b/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json new file mode 100644 index 00000000000..16733f9d05b --- /dev/null +++ b/cmd/infracost/testdata/breakdown_arm_template_config_file/standard_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/examples/arm/managed_disk/config.yml b/examples/arm/managed_disk/config.yml new file mode 100644 index 00000000000..bc10c482d6b --- /dev/null +++ b/examples/arm/managed_disk/config.yml @@ -0,0 +1,4 @@ +version: 0.1 +projects: + - path: ./examples/arm/managed_disk//standard_disk.json + name : standard_disk \ No newline at end of file diff --git a/examples/arm/managed_disk/standard_disk.json b/examples/arm/managed_disk/standard_disk.json new file mode 100644 index 00000000000..16733f9d05b --- /dev/null +++ b/examples/arm/managed_disk/standard_disk.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/azure/azure.go b/internal/providers/arm/azure/azure.go new file mode 100644 index 00000000000..7628afbf802 --- /dev/null +++ b/internal/providers/arm/azure/azure.go @@ -0,0 +1,19 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/schema" +) + +var DefaultProviderRegion = "eastus" + +func GetDefaultRefIDFunc(d *schema.ResourceData) []string { + return []string{d.Get("id").String()} +} + +func DefaultCloudResourceIDFunc(d *schema.ResourceData) []string { + return []string{} +} + +func GetSpecialContext(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{} +} diff --git a/internal/providers/arm/azure/linux_virtual_machine.go b/internal/providers/arm/azure/linux_virtual_machine.go new file mode 100644 index 00000000000..c6361194476 --- /dev/null +++ b/internal/providers/arm/azure/linux_virtual_machine.go @@ -0,0 +1,35 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" +) + +func getLinuxVirtualMachineRegistryItem() *schema.RegistryItem { + return &schema.RegistryItem{ + Name: "Microsoft.Compute/virtualMachines/Linux", + CoreRFunc: NewAzureLinuxVirtualMachine, + Notes: []string{ + "Non-standard images such as RHEL are not supported.", + "Low priority, Spot and Reserved instances are not supported.", + }, + } +} +func NewAzureLinuxVirtualMachine(d *schema.ResourceData) schema.CoreResource { + r := &azure.LinuxVirtualMachine{ + Address: d.Address, + Region: d.Region, + Size: d.Get("properties.hardwareProfile.vmSize").String(), + UltraSSDEnabled: d.Get("properties.additionalCapabilities.ultraSSDEnabled").Bool(), + } + + if len(d.Get("properties.storageProfile.osDisk").Array()) > 0 { + storageData := d.Get("properties.storageProfile.osDisk").Array()[0] + r.OSDiskData = &azure.ManagedDiskData{ + DiskType: storageData.Get("managedDisk.storageAccountType").String(), + DiskSizeGB: storageData.Get("diskSizeGB").Int(), + } + } + + return r +} diff --git a/internal/providers/arm/azure/managed_disk.go b/internal/providers/arm/azure/managed_disk.go new file mode 100644 index 00000000000..3cb4ef5ab9c --- /dev/null +++ b/internal/providers/arm/azure/managed_disk.go @@ -0,0 +1,28 @@ +package azure + +import ( + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" +) + +func getManagedDiskRegistryItem() *schema.RegistryItem { + return &schema.RegistryItem{ + Name: "Microsoft.Compute/disks", + CoreRFunc: NewManagedDisk, + } +} + +func NewManagedDisk(d *schema.ResourceData) schema.CoreResource { + r := &azure.ManagedDisk{ + Address: d.Address, + Region: d.Region, + ManagedDiskData: azure.ManagedDiskData{ + DiskType: d.Get("sku.name").String(), + DiskSizeGB: d.Get("properties.diskSizeGB").Int(), + DiskIOPSReadWrite: d.Get("properties.diskIOPSReadWrite").Int(), + DiskMBPSReadWrite: d.Get("properties.diskMBpsReadWrite").Int(), + }, + } + + return r +} diff --git a/internal/providers/arm/azure/registry.go b/internal/providers/arm/azure/registry.go new file mode 100644 index 00000000000..a641784bed0 --- /dev/null +++ b/internal/providers/arm/azure/registry.go @@ -0,0 +1,670 @@ +package azure + +import "github.com/infracost/infracost/internal/schema" + +// ResourceRegistry grouped alphabetically +var ResourceRegistry []*schema.RegistryItem = []*schema.RegistryItem{ + // getActiveDirectoryDomainServiceRegistryItem(), + // getActiveDirectoryDomainServiceReplicaSetRegistryItem(), + // getAPIManagementRegistryItem(), + // getApplicationGatewayRegistryItem(), + // getAppServiceEnvironmentRegistryItem(), + // GetAzureRMAppIntegrationServiceEnvironmentRegistryItem(), + // getFunctionAppRegistryItem(), + // GetAzureRMAppNATGatewayRegistryItem(), + // getAppServiceCertificateBindingRegistryItem(), + // getAppServiceCertificateOrderRegistryItem(), + // getAppServiceCustomHostnameBindingRegistryItem(), + // getAppServicePlanRegistryItem(), + // getApplicationInsightsWebTestRegistryItem(), + // getApplicationInsightsRegistryItem(), + // getAutomationAccountRegistryItem(), + // getAutomationDSCConfigurationRegistryItem(), + // getAutomationDSCNodeConfigurationRegistryItem(), + // getAutomationJobScheduleRegistryItem(), + // getBastionHostRegistryItem(), + // GetAzureRMCDNEndpointRegistryItem(), + // getContainerRegistryRegistryItem(), + // getCosmosDBAccountRegistryItem(), + // GetAzureRMCosmosdbCassandraKeyspaceRegistryItem(), + // GetAzureRMCosmosdbCassandraTableRegistryItem(), + // GetAzureRMCosmosdbGremlinDatabaseRegistryItem(), + // GetAzureRMCosmosdbGremlinGraphRegistryItem(), + // GetAzureRMCosmosdbMongoCollectionRegistryItem(), + // GetAzureRMCosmosdbMongoDatabaseRegistryItem(), + // GetAzureRMCosmosdbSQLContainerRegistryItem(), + // GetAzureRMCosmosdbSQLDatabaseRegistryItem(), + // GetAzureRMCosmosdbTableRegistryItem(), + // getDatabricksWorkspaceRegistryItem(), + // getDNSARecordRegistryItem(), + // getDNSAAAARecordRegistryItem(), + // getDNSCAARecordRegistryItem(), + // getDNSCNameRecordRegistryItem(), + // getDNSMXRecordRegistryItem(), + // getDNSNSRecordRegistryItem(), + // getDNSPtrRecordRegistryItem(), + // getDNSSrvRecordRegistryItem(), + // getDNSTxtRecordRegistryItem(), + // getDNSPrivateZoneRegistryItem(), + // getDNSZoneRegistryItem(), + // GetAzureRMEventHubsNamespaceRegistryItem(), + // getExpressRouteConnectionRegistryItem(), + // getExpressRouteGatewayRegistryItem(), + // GetAzureRMFirewallRegistryItem(), + // getAzureRMFirewallPolicyRegistryItem(), + // getAzureRMFirewallPolicyRuleCollectionGroupRegistryItem(), + // getFrontdoorFirewallPolicyRegistryItem(), + // getFrontdoorRegistryItem(), + // GetAzureRMHDInsightHadoopClusterRegistryItem(), + // GetAzureRMHDInsightHBaseClusterRegistryItem(), + // GetAzureRMHDInsightInteractiveQueryClusterRegistryItem(), + // GetAzureRMHDInsightKafkaClusterRegistryItem(), + // GetAzureRMHDInsightSparkClusterRegistryItem(), + // GetAzureRMKeyVaultCertificateRegistryItem(), + // GetAzureRMKeyVaultKeyRegistryItem(), + // GetAzureRMKeyVaultManagedHSMRegistryItem(), + // getKubernetesClusterRegistryItem(), + // getKubernetesClusterNodePoolRegistryItem(), + // getLoadBalancerRegistryItem(), + // GetAzureRMLoadBalancerRuleRegistryItem(), + // GetAzureRMLoadBalancerOutboundRuleRegistryItem(), + // getLinuxFunctionAppRegistryItem(), + getLinuxVirtualMachineRegistryItem(), + // getLinuxVirtualMachineScaleSetRegistryItem(), + // getLogAnalyticsWorkspaceRegistryItem(), + getManagedDiskRegistryItem(), + // GetAzureRMMariaDBServerRegistryItem(), + // getMSSQLDatabaseRegistryItem(), + // GetAzureRMMySQLServerRegistryItem(), + // GetAzureRMNotificationHubNamespaceRegistryItem(), + // getPointToSiteVpnGatewayRegistryItem(), + // getPostgreSQLFlexibleServerRegistryItem(), + // GetAzureRMPostgreSQLServerRegistryItem(), + // getPrivateDNSARecordRegistryItem(), + // getPrivateDNSAAAARecordRegistryItem(), + // getPrivateDNSCNameRecordRegistryItem(), + // getPrivateDNSMXRecordRegistryItem(), + // getPrivateDNSPTRRecordRegistryItem(), + // getPrivateDNSSRVRecordRegistryItem(), + // getPrivateDNSTXTRecordRegistryItem(), + // GetAzureRMPrivateEndpointRegistryItem(), + // GetAzureRMPublicIPRegistryItem(), + // GetAzureRMPublicIPPrefixRegistryItem(), + // GetAzureRMSearchServiceRegistryItem(), + // GetAzureRMRedisCacheRegistryItem(), + // getAzureRMMSSQLManagedInstanceRegistryItem(), + // getStorageAccountRegistryItem(), + // getSQLDatabaseRegistryItem(), + // getSQLManagedInstanceRegistryItem(), + // GetAzureRMSynapseSparkPoolRegistryItem(), + // GetAzureRMSynapseSQLPoolRegistryItem(), + // GetAzureRMSynapseWorkspacRegistryItem(), + // getVirtualHubRegistryItem(), + // getVirtualMachineScaleSetRegistryItem(), + // getVirtualMachineRegistryItem(), + // GetAzureRMVirtualNetworkGatewayConnectionRegistryItem(), + // GetAzureRMVirtualNetworkGatewayRegistryItem(), + // getWindowsVirtualMachineRegistryItem(), + // getWindowsVirtualMachineScaleSetRegistryItem(), + // getVPNGatewayRegistryItem(), + // getVPNGatewayConnectionRegistryItem(), + // getDataFactoryRegistryItem(), + // getDataFactoryIntegrationRuntimeAzureRegistryItem(), + // getDataFactoryIntegrationRuntimeAzureSSISRegistryItem(), + // getDataFactoryIntegrationRuntimeManagedRegistryItem(), + // getDataFactoryIntegrationRuntimeSelfHostedRegistryItem(), + // getLogAnalyticsSolutionRegistryItem(), + // getMySQLFlexibleServerRegistryItem(), + // getServicePlanRegistryItem(), + // getSentinelDataConnectorAwsCloudTrailRegistryItem(), + // getSentinelDataConnectorAzureActiveDirectoryRegistryItem(), + // getSentinelDataConnectorAzureAdvancedThreatProtectionRegistryItem(), + // getSentinelDataConnectorAzureSecurityCenterRegistryItem(), + // getSentinelDataConnectorMicrosoftCloudAppSecurityRegistryItem(), + // getSentinelDataConnectorMicrosoftDefenderAdvancedThreatProtectionRegistryItem(), + // getSentinelDataConnectorOffice365RegistryItem(), + // getSentinelDataConnectorThreatIntelligenceRegistryItem(), + // getIoTHubRegistryItem(), + // getIoTHubDPSRegistryItem(), + // getVirtualNetworkPeeringRegistryItem(), + // geWindowsFunctionAppRegistryItem(), + // getPowerBIEmbeddedRegistryItem(), + // getMSSQLElasticPoolRegistryItem(), + // getSQLElasticPoolRegistryItem(), + // getMonitorActionGroupRegistryItem(), + // getMonitorDataCollectionRuleRegistryItem(), + // getMonitorDiagnosticSettingRegistryItem(), + // getMonitorMetricAlertRegistryItem(), + // getMonitorScheduledQueryRulesAlertRegistryItem(), + // getMonitorScheduledQueryRulesAlertV2RegistryItem(), + // getApplicationInsightsStandardWebTestRegistryItem(), + // getRecoveryServicesVaultRegistryItem(), + // getBackupProtectedVmRegistryItem(), + // getStorageManagementPolicyRegistryItem(), + // getStorageQueueRegistryItem(), + // getStorageShareRegistryItem(), + // getLogicAppIntegrationAccountRegistryItem(), + // getSignalRServiceRegistryItem(), + // getTrafficManagerProfileRegistryItem(), + // getTrafficManagerAzureEndpointRegistryItem(), + // getTrafficManagerExternalEndpointRegistryItem(), + // getTrafficManagerNestedEndpointRegistryItem(), + // getEventgridSystemTopicRegistryItem(), + // getEventgridTopicRegistryItem(), + // getSecurityCenterSubscriptionPricingRegistryItem(), + // getNetworkWatcherFlowLogRegistryItem(), + // getNetworkWatcherRegistryItem(), + // getNetworkConnectionMonitorRegistryItem(), + // getServiceBusNamespaceRegistryItem(), + // getLogicAppStandardRegistryItem(), + // getImageRegistryItem(), + // getSnapshotRegistryItem(), + // getPrivateDnsResolverInboundEndpointRegistryItem(), + // getPrivateDnsResolverOutboundEndpointRegistryItem(), + // getPrivateDnsResolverDnsForwardingRulesetRegistryItem(), + // getMachineLearningComputeInstanceRegistryItem(), + // getMachineLearningComputeClusterRegistryItem(), + // getNetworkDdosProtectionPlanRegistryItem(), + // getAppConfigurationRegistryItem(), + // getFederatedIdentityCredentialRegistryItem(), + // getCognitiveAccountRegistryItem(), + // getCognitiveDeploymentRegistryItem(), +} + +// FreeResources grouped alphabetically +var FreeResources = []string{ + // Azure App Configuration + "azurerm_app_configuration_feature", + "azurerm_app_configuration_key", + // Azure AI Services + "azurerm_cognitive_account_customer_managed_key", + // Azure Api Management + "azurerm_api_management_api", + "azurerm_api_management_api_diagnostic", + "azurerm_api_management_api_operation", + "azurerm_api_management_api_operation_policy", + "azurerm_api_management_api_operation_tag", + "azurerm_api_management_api_policy", + "azurerm_api_management_api_schema", + "azurerm_api_management_api_version_set", + "azurerm_api_management_authorization_server", + "azurerm_api_management_backend", + "azurerm_api_management_certificate", + "azurerm_api_management_custom_domain", + "azurerm_api_management_diagnostic", + "azurerm_api_management_email_template", + "azurerm_api_management_group", + "azurerm_api_management_group_user", + "azurerm_api_management_identity_provider_aad", + "azurerm_api_management_identity_provider_aadb2c", + "azurerm_api_management_identity_provider_facebook", + "azurerm_api_management_identity_provider_google", + "azurerm_api_management_identity_provider_microsoft", + "azurerm_api_management_identity_provider_twitter", + "azurerm_api_management_logger", + "azurerm_api_management_named_value", + "azurerm_api_management_openid_connect_provider", + "azurerm_api_management_policy", + "azurerm_api_management_product", + "azurerm_api_management_product_api", + "azurerm_api_management_product_group", + "azurerm_api_management_product_policy", + "azurerm_api_management_property", + "azurerm_api_management_subscription", + "azurerm_api_management_user", + + // Azure Application Gateway + "azurerm_web_application_firewall_policy", + + // Azure App Service + "azurerm_app_service", + "azurerm_app_service_active_slot", + "azurerm_app_service_certificate", + "azurerm_app_service_managed_certificate", + "azurerm_app_service_slot", + "azurerm_app_service_slot_virtual_network_swift_connection", + "azurerm_app_service_source_control_token", + "azurerm_app_service_virtual_network_swift_connection", + + // Azure Attestation + "azurerm_attestation_provider", + + // Azure Automation + "azurerm_automation_certificate", + "azurerm_automation_connection", + "azurerm_automation_connection_certificate", + "azurerm_automation_connection_type", + "azurerm_automation_connection_classic_certificate", + "azurerm_automation_connection_service_principal", + "azurerm_automation_credential", + "azurerm_automation_hybrid_runbook_worker", + "azurerm_automation_hybrid_runbook_worker_group", + "azurerm_automation_module", + "azurerm_automation_runbook", + "azurerm_automation_schedule", + "azurerm_automation_software_update_configuration", + "azurerm_automation_source_control", + "azurerm_automation_variable_bool", + "azurerm_automation_variable_datetime", + "azurerm_automation_variable_int", + "azurerm_automation_variable_string", + "azurerm_automation_webhook", + + // Azure Backup & Recovery Services Vault + "azurerm_backup_policy_vm", + "azurerm_backup_policy_file_share", + "azurerm_site_recovery_network_mapping", + "azurerm_site_recovery_replication_policy", + + // Azure Base + "azurerm_resource_group", + "azurerm_resource_provider_registration", + "azurerm_subscription", + "azurerm_role_assignment", + "azurerm_role_definition", + "azurerm_user_assigned_identity", + + // Azure Blueprints + "azurerm_blueprint_assignment", + + // Azure CDN + "azurerm_cdn_frontdoor_custom_domain_association", + "azurerm_cdn_profile", + + // Azure Consumption + "azurerm_consumption_budget_management_group", + "azurerm_consumption_budget_resource_group", + "azurerm_consumption_budget_subscription", + + // Azure CosmosDB + "azurerm_cosmosdb_notebook_workspace", + "azurerm_cosmosdb_sql_role_assignment", + "azurerm_cosmosdb_sql_role_definition", + "azurerm_cosmosdb_sql_stored_procedure", + "azurerm_cosmosdb_sql_trigger", + "azurerm_cosmosdb_sql_user_defined_function", + + // Azure Cost Management + "azurerm_cost_anomaly_alert", + "azurerm_cost_management_scheduled_action", + "azurerm_resource_group_cost_management_export", + "azurerm_resource_group_cost_management_view", + "azurerm_subscription_cost_management_export", + "azurerm_subscription_cost_management_view", + + // Azure DNS + "azurerm_private_dns_zone_virtual_network_link", + "azurerm_private_dns_resolver", + + // Azure Dev Test + "azurerm_dev_test_global_vm_shutdown_schedule", + "azurerm_dev_test_policy", + "azurerm_dev_test_schedule", + "azurerm_dev_test_lab", + + // Azure Data Factory + "azurerm_data_factory_custom_dataset", + "azurerm_data_factory_data_flow", + "azurerm_data_factory_dataset_azure_blob", + "azurerm_data_factory_dataset_binary", + "azurerm_data_factory_dataset_cosmosdb_sqlapi", + "azurerm_data_factory_dataset_delimited_text", + "azurerm_data_factory_dataset_http", + "azurerm_data_factory_dataset_json", + "azurerm_data_factory_dataset_mysql", + "azurerm_data_factory_dataset_parquet", + "azurerm_data_factory_dataset_postgresql", + "azurerm_data_factory_dataset_snowflake", + "azurerm_data_factory_dataset_sql_server_table", + "azurerm_data_factory_linked_custom_service", + "azurerm_data_factory_linked_service_azure_blob_storage", + "azurerm_data_factory_linked_service_azure_databricks", + "azurerm_data_factory_linked_service_azure_file_storage", + "azurerm_data_factory_linked_service_azure_function", + "azurerm_data_factory_linked_service_azure_search", + "azurerm_data_factory_linked_service_azure_sql_database", + "azurerm_data_factory_linked_service_azure_table_storage", + "azurerm_data_factory_linked_service_cosmosdb", + "azurerm_data_factory_linked_service_cosmosdb_mongoapi", + "azurerm_data_factory_linked_service_data_lake_storage_gen2", + "azurerm_data_factory_linked_service_key_vault", + "azurerm_data_factory_linked_service_kusto", + "azurerm_data_factory_linked_service_mysql", + "azurerm_data_factory_linked_service_odata", + "azurerm_data_factory_linked_service_odbc", + "azurerm_data_factory_linked_service_postgresql", + "azurerm_data_factory_linked_service_sftp", + "azurerm_data_factory_linked_service_snowflake", + "azurerm_data_factory_linked_service_sql_server", + "azurerm_data_factory_linked_service_synapse", + "azurerm_data_factory_linked_service_web", + "azurerm_data_factory_managed_private_endpoint", + "azurerm_data_factory_pipeline", + "azurerm_data_factory_trigger_blob_event", + "azurerm_data_factory_trigger_custom_event", + "azurerm_data_factory_trigger_schedule", + "azurerm_data_factory_tumbling_window", + + // Azure Database + "azurerm_mariadb_configuration", + "azurerm_mariadb_database", + "azurerm_mariadb_firewall_rule", + "azurerm_mariadb_virtual_network_rule", + + "azurerm_mysql_active_directory_administrator", + "azurerm_mysql_configuration", + "azurerm_mysql_database", + "azurerm_mysql_firewall_rule", + "azurerm_mysql_flexible_database", + "azurerm_mysql_flexible_server_configuration", + "azurerm_mysql_flexible_server_firewall_rule", + "azurerm_mysql_server_key", + "azurerm_mysql_virtual_network_rule", + + "azurerm_postgresql_active_directory_administrator", + "azurerm_postgresql_configuration", + "azurerm_postgresql_database", + "azurerm_postgresql_firewall_rule", + "azurerm_postgresql_flexible_server_active_directory_administrator", + "azurerm_postgresql_flexible_server_configuration", + "azurerm_postgresql_flexible_server_database", + "azurerm_postgresql_flexible_server_firewall_rule", + "azurerm_postgresql_server_key", + "azurerm_postgresql_virtual_network_rule", + + // Azure Datalake Gen 2 + "azurerm_storage_data_lake_gen2_filesystem", + + // Azure Event Grid + "azurerm_eventgrid_domain", + "azurerm_eventgrid_event_subscription", + "azurerm_eventgrid_system_topic_event_subscription", + + // Azure Event Hub + "azurerm_eventhub", + "azurerm_eventhub_authorization_rule", + "azurerm_eventhub_cluster", + "azurerm_eventhub_consumer_group", + "azurerm_eventhub_namespace_authorization_rule", + "azurerm_eventhub_namespace_customer_managed_key", + "azurerm_eventhub_namespace_disaster_recovery_config", + + // Azure Firewall + "azurerm_firewall_application_rule_collection", + "azurerm_firewall_nat_rule_collection", + "azurerm_firewall_network_rule_collection", + + // Azure Front Door + "azurerm_frontdoor_custom_https_configuration", + "azurerm_frontdoor_rules_engine", + + // Azure Key Vault + "azurerm_key_vault", + "azurerm_key_vault_access_policy", + "azurerm_key_vault_certificate_data", + "azurerm_key_vault_certificate_issuer", + "azurerm_key_vault_secret", + + // Azure IoT + "azurerm_iothub_certificate", + "azurerm_iothub_consumer_group", + "azurerm_iothub_dps_certificate", + "azurerm_iothub_dps_shared_access_policy", + "azurerm_iothub_endpoint_eventhub", + "azurerm_iothub_enrichment", + "azurerm_iothub_route", + "azurerm_iothub_shared_access_policy", + + // Azure Lighthouse (Delegated Resource Management) + "azurerm_lighthouse_definition", + "azurerm_lighthouse_assignment", + + // Azure Load Balancer + "azurerm_lb_backend_address_pool", + "azurerm_lb_backend_address_pool_address", + "azurerm_lb_nat_pool", + "azurerm_lb_nat_rule", + "azurerm_lb_probe", + + // Azure Logic App + "azurerm_logic_app_action_custom", + "azurerm_logic_app_action_http", + "azurerm_logic_app_integration_account_agreement", + "azurerm_logic_app_integration_account_assembly", + "azurerm_logic_app_integration_account_batch_configuration", + "azurerm_logic_app_integration_account_certificate", + "azurerm_logic_app_integration_account_map", + "azurerm_logic_app_integration_account_partner", + "azurerm_logic_app_integration_account_schema", + "azurerm_logic_app_integration_account_session", + "azurerm_logic_app_trigger_custom", + "azurerm_logic_app_trigger_http_request", + "azurerm_logic_app_trigger_recurrence", + "azurerm_logic_app_workflow", + + // Azure Machine Learning + "azurerm_machine_learning_workspace", + + // Azure Management + "azurerm_management_group", + "azurerm_management_group_subscription_association", + "azurerm_management_lock", + + // Azure Managed Applications + "azurerm_managed_application", + "azurerm_managed_application_definition", + + // Azure Monitor + "azurerm_monitor_aad_diagnostic_setting", + "azurerm_monitor_action_rule_action_group", + "azurerm_monitor_action_rule_suppression", + "azurerm_monitor_activity_log_alert", + "azurerm_monitor_alert_processing_rule_action_group", + "azurerm_monitor_alert_processing_rule_suppression", + "azurerm_monitor_autoscale_setting", + "azurerm_monitor_data_collection", + "azurerm_monitor_data_collection_rule_association", + "azurerm_monitor_log_profile", + "azurerm_monitor_private_link_scope", + "azurerm_monitor_private_link_scoped_service", + "azurerm_monitor_scheduled_query_rules_log", + "azurerm_monitor_smart_detector_alert_rule", + + // Azure Monitor - Application Insights + "azurerm_application_insights_analytics_item", + "azurerm_application_insights_api_key", + "azurerm_application_insights_smart_detection_rule", + "azurerm_application_insights_workbook", + "azurerm_application_insights_workbook_template", + + // Azure Monitor - Log Analytics + "azurerm_log_analytics_cluster_customer_managed_key", + "azurerm_log_analytics_data_export_rule", + "azurerm_log_analytics_datasource_windows_event", + "azurerm_log_analytics_datasource_windows_performance_counter", + "azurerm_log_analytics_linked_service", + "azurerm_log_analytics_linked_storage_account", + "azurerm_log_analytics_query_pack", + "azurerm_log_analytics_query_pack_query", + "azurerm_log_analytics_saved_search", + "azurerm_log_analytics_storage_insights", + + // Azure Networking + "azurerm_application_security_group", + "azurerm_ip_group", + "azurerm_local_network_gateway", + "azurerm_nat_gateway_public_ip_association", + "azurerm_nat_gateway_public_ip_prefix_association", + "azurerm_network_interface", + "azurerm_network_interface_application_gateway_backend_address_pool_association", + "azurerm_network_interface_application_security_group_association", + "azurerm_network_interface_backend_address_pool_association", + "azurerm_network_interface_nat_rule_association", + "azurerm_network_interface_security_group_association", + "azurerm_network_security_group", + "azurerm_network_security_rule", + "azurerm_private_link_service", + "azurerm_route", + "azurerm_route_filter", + "azurerm_route_map", + "azurerm_route_table", + "azurerm_storage_account_local_user", + "azurerm_storage_account_network_rules", + "azurerm_subnet", + "azurerm_subnet_nat_gateway_association", + "azurerm_subnet_network_security_group_association", + "azurerm_subnet_route_table_association", + "azurerm_subnet_service_endpoint_storage_policy", + "azurerm_virtual_network", + "azurerm_virtual_network_dns_servers", + + // Azure Notification Hub + "azurerm_notification_hub", + + // Azure Policy + "azurerm_policy_assignment", + "azurerm_policy_definition", + "azurerm_policy_remediation", + "azurerm_policy_set_definition", + "azurerm_subscription_policy_assignment", + "azurerm_subscription_policy_exemption", + "azurerm_subscription_policy_remediation", + "azurerm_resource_group_policy_assignment", + "azurerm_resource_group_policy_exemption", + "azurerm_resource_group_policy_remediation", + "azurerm_management_group_policy_exemption", + "azurerm_management_group_policy_assignment", + "azurerm_management_group_policy_remediation", + + // Azure Portal + "azurerm_dashboard", + "azurerm_portal_dashboard", + + // Azure Redis + "azurerm_redis_firewall_rule", + "azurerm_redis_linked_server", + + // Azure Registry + "azurerm_container_registry_scope_map", + "azurerm_container_registry_token", + "azurerm_container_registry_webhook", + + // Azure Sentinel + "azurerm_sentinel_alert_rule_machine_learning_behavior_analytics", + "azurerm_sentinel_alert_rule_fusion", + "azurerm_sentinel_alert_rule_ms_security_incident", + "azurerm_sentinel_alert_rule_scheduled", + + // Azure Service Bus + "azurerm_servicebus_namespace_authorization_rule", + "azurerm_servicebus_namespace_disaster_recovery_config", + "azurerm_servicebus_namespace_network_rule_set", + "azurerm_servicebus_queue", + "azurerm_servicebus_queue_authorization_rule", + "azurerm_servicebus_subscription", + "azurerm_servicebus_subscription_rule", + "azurerm_servicebus_topic", + "azurerm_servicebus_topic_authorization_rule", + "azurerm_relay_hybrid_connection_authorization_rule", + "azurerm_relay_namespace_authorization_rule", + + // Azure Shared Image Gallery + "azurerm_shared_image", + "azurerm_shared_image_gallery", + + // Azure SignalR + "azurerm_signalr_service_network_acl", + "azurerm_signalr_shared_private_link", + + // Azure Site Recovery + "azurerm_site_recovery_protection_container_mapping", + + // Azure SQL + "azurerm_sql_failover_group", + "azurerm_sql_firewall_rule", + "azurerm_sql_server", + "azurerm_sql_virtual_network_rule", + + "azurerm_mssql_database_extended_auditing_policy", + "azurerm_mssql_database_vulnerability_assessment_rule_baseline", + "azurerm_mssql_failover_group", + "azurerm_mssql_firewall_rule", + "azurerm_mssql_job_agent", + "azurerm_mssql_job_credential", + "azurerm_mssql_managed_instance_active_directory_administrator", + "azurerm_mssql_managed_instance_security_alert_policy", + "azurerm_mssql_managed_instance_transparent_data_encryption", + "azurerm_mssql_managed_instance_vulnerability_assessment", + "azurerm_mssql_outbound_firewall_rule", + "azurerm_mssql_server", + "azurerm_mssql_server_dns_alias", + "azurerm_mssql_server_extended_auditing_policy", + "azurerm_mssql_server_microsoft_support_auditing_policy", + "azurerm_mssql_server_security_alert_policy", + "azurerm_mssql_server_transparent_data_encryption", + "azurerm_mssql_server_vulnerability_assessment", + "azurerm_mssql_virtual_network_rule", + + // Azure Storage + "azurerm_storage_account_customer_managed_key", + "azurerm_storage_account_local_user", + "azurerm_storage_account_network_rules", + "azurerm_storage_blob", + "azurerm_storage_blob_inventory_policy", + "azurerm_storage_container", + "azurerm_storage_data_lake_gen2_path", + "azurerm_storage_object_replication", + "azurerm_storage_share_directory", + "azurerm_storage_share_file", + "azurerm_storage_sync_cloud_endpoint", + "azurerm_storage_sync_group", + "azurerm_storage_table_entity", + + // Azure Virtual Desktop + "azurerm_virtual_desktop_application", + "azurerm_virtual_desktop_application_group", + "azurerm_virtual_desktop_workspace", + "azurerm_virtual_desktop_workspace_application_group_association", + "azurerm_virtual_desktop_host_pool", + "azurerm_virtual_desktop_host_pool_registration_info", + + // Azure Service Plan + "azurerm_windows_web_app", + "azurerm_linux_web_app", + + // Azure Synapse Analytics + "azurerm_synapse_firewall_rule", + "azurerm_synapse_private_link_hub", + + // Azure Virtual Hub + "azurerm_virtual_hub_route_table", + "azurerm_virtual_hub_route_table_route", + + // Azure Virtual Machines + "azurerm_virtual_machine_data_disk_attachment", + "azurerm_virtual_machine_extension", + "azurerm_virtual_machine_scale_set_extension", + "azurerm_availability_set", + "azurerm_proximity_placement_group", + "azurerm_ssh_public_key", + "azurerm_marketplace_agreement", + + // Azure WAN + "azurerm_virtual_hub_connection", + "azurerm_virtual_wan", + "azurerm_vpn_server_configuration", + + // Microsoft Defender for Cloud + "azurerm_security_center_automation", + "azurerm_security_center_server_vulnerability_assessment", + "azurerm_security_center_assessment", + "azurerm_security_center_assessment_policy", + "azurerm_security_center_auto_provisioning", + "azurerm_security_center_automation", + "azurerm_security_center_contact", + "azurerm_security_center_server_vulnerability_assessment_virtual_machine", + "azurerm_security_center_setting", + "azurerm_security_center_workspace", +} + +var UsageOnlyResources = []string{} diff --git a/internal/providers/arm/parser.go b/internal/providers/arm/parser.go new file mode 100644 index 00000000000..964345af2e2 --- /dev/null +++ b/internal/providers/arm/parser.go @@ -0,0 +1,149 @@ +package arm + +import ( + "strings" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/providers/arm/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/tidwall/gjson" +) + +type Parser struct { + ctx *config.ProjectContext + includePastResources bool +} + +func NewParser(ctx *config.ProjectContext, includePastResources bool) *Parser { + return &Parser{ctx: ctx, includePastResources: includePastResources} +} + +type parsedResource struct { + PartialResource *schema.PartialResource + ResourceData *schema.ResourceData +} + +func (p *Parser) ParseJSON(data gjson.Result, usage schema.UsageMap) ([]*parsedResource, error) { + parsedResources := []*parsedResource{} + + resourceData, _ := p.parseResourceData(&data) + + p.populateUsageData(resourceData, usage) + + for _, d := range resourceData { + + parsedResource := p.createParsedResource(d, d.UsageData) + parsedResources = append(parsedResources, &parsedResource) + + } + return parsedResources, nil +} + +func (p *Parser) createParsedResource(d *schema.ResourceData, u *schema.UsageData) parsedResource { + for cKey, cValue := range azure.GetSpecialContext(d) { + p.ctx.ContextValues.SetValue(cKey, cValue) + } + + if registryItem, ok := (*ResourceRegistryMap)[d.Type]; ok { + if registryItem.NoPrice { + resource := &schema.Resource{ + Name: d.Address, + IsSkipped: true, + NoPrice: true, + SkipMessage: "Free resource.", + Metadata: d.Metadata, + } + return parsedResource{ + PartialResource: schema.NewPartialResource(d, resource, nil, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + + } + + // Use the CoreRFunc to generate a CoreResource if possible. This is + // the new/preferred way to create provider-agnostic resources that + // support advanced features such as Infracost Cloud usage estimates + // and actual costs. + if registryItem.CoreRFunc != nil { + coreRes := registryItem.CoreRFunc(d) + if coreRes != nil { + return parsedResource{ + PartialResource: schema.NewPartialResource(d, nil, coreRes, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + } + } else { + res := registryItem.RFunc(d, u) + if res != nil { + if u != nil { + res.EstimationSummary = u.CalcEstimationSummary() + } + + return parsedResource{ + PartialResource: schema.NewPartialResource(d, res, nil, registryItem.CloudResourceIDFunc(d)), + ResourceData: d, + } + } + } + } + + return parsedResource{ + PartialResource: schema.NewPartialResource( + d, + &schema.Resource{ + Name: d.Address, + IsSkipped: true, + SkipMessage: "This resource is not currently supported", + Metadata: d.Metadata, + }, + nil, + []string{}, + ), + ResourceData: d, + } +} + +func (p *Parser) parseResourceData(data *gjson.Result) (map[string]*schema.ResourceData, error) { + resources := make(map[string]*schema.ResourceData) + for _, res := range data.Array() { + t := res.Get("type").String() + if t == "Microsoft.Compute/virtualMachines" { + //There is no official naming for Linux or Windows virtual machines, so we need to modify the resource name to obtain the resource from the registry + GetOSResourceType(&t, &res) + } + // //New address will consist of the official Microsoft resource type, with an extra '/' at the end to seperate the resource type from the name + newAddress := strings.Clone(t) + "/" + res.Get("name").Str + resData := schema.NewResourceData(strings.Clone(t), "azurerm", newAddress, nil, res) + resData.Region = res.Get("location").String() + resources[strings.Clone(resData.Address)] = resData + } + return resources, nil +} + +/* +** +There doesn't seem to be an official Microsoft resource name that indicates whether the VM is Linux or Windows +So, we have to check the OS type from the properties and modified the register and mappping accordingly +In a previous version of the Infracost code, the azure_virtual_machine function used to check the os type and create a linux/windows virtual machine accordingly +** +*/ +func GetOSResourceType(resourceType *string, data *gjson.Result) { + name := "Microsoft.Compute/virtualMachines" + os := "Linux" + if data.Get("storage_image_reference.0.offer").Type != gjson.Null { + if strings.ToLower((data.Get("storage_image_reference.0.offer")).String()) == "windowsserver" { + os = "Windows" + } + } + if strings.ToLower((data.Get("storage_image_reference.0.offer")).String()) == "windows" { + os = "Windows" + } + *resourceType = name + "/" + os + +} + +func (p *Parser) populateUsageData(resData map[string]*schema.ResourceData, usage schema.UsageMap) { + for _, d := range resData { + d.UsageData = usage.Get(d.Address) + } +} diff --git a/internal/providers/arm/parser_test.go b/internal/providers/arm/parser_test.go new file mode 100644 index 00000000000..a8f15f98fae --- /dev/null +++ b/internal/providers/arm/parser_test.go @@ -0,0 +1,184 @@ +package arm + +import ( + "reflect" + "testing" + + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" +) + +var testData = `{ + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1", + "location": "francecentral", + "properties": { + "hardwareProfile": { + "vmSize": "standard_b1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "osProfile": { + "computerName": "standard_b1s", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + } + } + ] + } + ` + +func TestParseResourceData(t *testing.T) { + + expected := []schema.ResourceData{ + { + Type: "Microsoft.Compute/disks", + ProviderName: "azurerm", + Address: "Microsoft.Compute/disks/ultra", + }, + { + Type: "Microsoft.Compute/virtualMachines/Linux", + ProviderName: "azurerm", + Address: "Microsoft.Compute/virtualMachines/Linux/basic_b1", + }, + } + parser := Parser{} + data := gjson.Parse(testData).Get("resources") + resources, _ := parser.parseResourceData(&data) + for i := range expected { + assert.Equal(t, resources[expected[i].Address].Type, expected[i].Type) + assert.Equal(t, resources[expected[i].Address].ProviderName, expected[i].ProviderName) + assert.Equal(t, resources[expected[i].Address].Address, expected[i].Address) + } + +} + +func TestCreateParsedResoureceData(t *testing.T) { + + expected := []schema.CoreResource{ + &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + }, + // &azure.LinuxVirtualMachine{ + // Address: "Microsoft.Compute/virtualMachines", + // Region: "francecentral", + // Size: "standard_b1s", + // UltraSSDEnabled: false, + // OSDiskData: &azure.ManagedDiskData{ + // DiskType: "Standard_LRS", + // }, + // }, + } + resourceArray := gjson.Parse(testData).Get("resources").Array() + data := []map[string]*schema.ResourceData{ + { + "Microsoft.Compute/disks": &schema.ResourceData{ + Type: "Microsoft.Compute/disks", + ProviderName: "azurerm", + Region: "francecentral", + Address: "Microsoft.Compute/disks", + RawValues: resourceArray[0], + UsageData: &schema.UsageData{}, + }, + }, + // { + // "Microsoft.Compute/virtualMachines": &schema.ResourceData{ + // Type: "AZURE_Virtual_Machine_Linux", + // ProviderName: "azurerm", + // Address: "Microsoft.Compute/virtualMachines", + // RawValues: resourceArray[1], + // UsageData: &schema.UsageData{}, + // }, + // }, + } + parser := Parser{} + + for i := range data { + parsedResources := []*parsedResource{} + for _, d := range data[i] { + parsedData := parser.createParsedResource(d, d.UsageData) + parsedResources = append(parsedResources, &parsedData) + } + equal := reflect.DeepEqual(parsedResources[0].PartialResource.CoreResource, expected[i]) + assert.True(t, equal) + } + +} + +func TestParseJSON(t *testing.T) { + parser := Parser{} + data := gjson.Parse(testData).Get("resources") + expected := map[string]schema.CoreResource{ + "Microsoft.Compute/disks/ultra": &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks/ultra", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + }, + // &azure.LinuxVirtualMachine{ + // Address: "Microsoft.Compute/virtualMachines", + // Region: "francecentral", + // Size: "standard_b1s", + // UltraSSDEnabled: false, + // OSDiskData: &azure.ManagedDiskData{ + // DiskType: "Standard_LRS", + // }, + // }, + } + parsedResources, err := parser.ParseJSON(data, schema.UsageMap{}) + if err != nil { + assert.Fail(t, "Error occured while parsing JSON") + } + for i := range parsedResources { + res := parsedResources[i].PartialResource + ex := expected[res.Address] + equal := reflect.DeepEqual(res.CoreResource, ex) + assert.True(t, equal) + } + +} diff --git a/internal/providers/arm/registry.go b/internal/providers/arm/registry.go new file mode 100644 index 00000000000..35b9b42ff9f --- /dev/null +++ b/internal/providers/arm/registry.go @@ -0,0 +1,53 @@ +package arm + +import ( + "github.com/infracost/infracost/internal/providers/arm/azure" + "github.com/infracost/infracost/internal/schema" +) + +type RegistryItemMap map[string]*schema.RegistryItem + +var ( + ResourceRegistryMap = buildResourceRegistryMap() +) + +func buildResourceRegistryMap() *RegistryItemMap { + resourceRegistryMap := make(RegistryItemMap) + + for _, registryItem := range azure.ResourceRegistry { + if registryItem.CloudResourceIDFunc == nil { + registryItem.CloudResourceIDFunc = azure.DefaultCloudResourceIDFunc + } + resourceRegistryMap[registryItem.Name] = registryItem + resourceRegistryMap[registryItem.Name].DefaultRefIDFunc = azure.GetDefaultRefIDFunc + } + for _, registryItem := range createFreeResources(azure.FreeResources, azure.GetDefaultRefIDFunc, azure.DefaultCloudResourceIDFunc) { + resourceRegistryMap[registryItem.Name] = registryItem + } + + return &resourceRegistryMap +} + +// GetRegion returns the region lookup function for the given resource data type if it exists. +func (r *RegistryItemMap) GetRegion(resourceDataType string) schema.RegionLookupFunc { + item, ok := (*r)[resourceDataType] + if ok { + return item.GetRegion + } + + return nil +} + +func createFreeResources(l []string, defaultRefsFunc schema.ReferenceIDFunc, resourceIdFunc schema.CloudResourceIDFunc) []*schema.RegistryItem { + freeResources := make([]*schema.RegistryItem, 0) + for _, resourceName := range l { + freeResources = append(freeResources, &schema.RegistryItem{ + Name: resourceName, + NoPrice: true, + Notes: []string{"Free resource."}, + DefaultRefIDFunc: defaultRefsFunc, + CloudResourceIDFunc: resourceIdFunc, + }) + } + return freeResources +} diff --git a/internal/providers/arm/template_provider.go b/internal/providers/arm/template_provider.go new file mode 100644 index 00000000000..d8aff5d5aa2 --- /dev/null +++ b/internal/providers/arm/template_provider.go @@ -0,0 +1,204 @@ +package arm + +import ( + "encoding/json" + "log" + "os" + "regexp" + "strings" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/logging" + "github.com/infracost/infracost/internal/schema" + "github.com/tidwall/gjson" +) + +type TemplateProvider struct { + ctx *config.ProjectContext + Path string + includePastResources bool + content Content +} + +type Content struct { + FileContents map[string]FileContent + MergedBytes []byte +} + +type FileContent struct { + Schema string `json:"$schema"` + Parameters map[string]interface{} `json:"parameters"` + Variables map[string]interface{} `json:"variables"` + ContentVersion string `json:"contentVersion"` + Resources []interface{} `json:"resources"` +} + +func NewTemplateProvider(ctx *config.ProjectContext, includePastResources bool, path string) *TemplateProvider { + return &TemplateProvider{ + ctx: ctx, + Path: path, + includePastResources: includePastResources, + content: Content{FileContents: map[string]FileContent{}}, + } +} + +func (p *TemplateProvider) Type() string { + return "arm" +} +func (p *TemplateProvider) Context() *config.ProjectContext { return p.ctx } + +func (p *TemplateProvider) DisplayType() string { + return "Azure Resource Manager" +} + +func (p *TemplateProvider) AddMetadata(metadata *schema.ProjectMetadata) { + //no op +} + +func (p *TemplateProvider) ProjectName() string { + return config.CleanProjectName(p.ctx.ProjectConfig.Path) +} + +func (p *TemplateProvider) RelativePath() string { + return p.ctx.ProjectConfig.Path +} + +func (p *TemplateProvider) VarFiles() []string { + return nil +} + +func (p *TemplateProvider) LoadResources(usage schema.UsageMap) ([]*schema.Project, error) { + + logging.Logger.Debug().Msg("Extracting only cost-related params from arm template") + + rootPath := p.ctx.ProjectConfig.Path + if rootPath == "" { + log.Fatal("Root path is not provided") + } + + projects := make([]*schema.Project, 0) + + //Merge all the resources from the files in the directory + p.MergeFileResources(p.Path) + + p.content.MergeBytes() + + project, _ := p.loadProject(p.Path, usage) + projects = append(projects, project) + + return projects, nil + +} + +func (p *TemplateProvider) loadProject(filePath string, usage schema.UsageMap) (*schema.Project, error) { + + metadata := schema.DetectProjectMetadata(filePath) + metadata.Type = p.Type() + p.AddMetadata(metadata) + name := p.ctx.ProjectConfig.Name + if name == "" { + name = metadata.GenerateProjectName(p.ctx.RunContext.VCSMetadata.Remote, p.ctx.RunContext.IsCloudEnabled()) + } + + project := schema.NewProject(name, metadata) + p.parseFiles(project, usage) + p.content.MergedBytes = nil + return project, nil +} + +func (p *TemplateProvider) parseFiles(project *schema.Project, usage schema.UsageMap) { + parser := NewParser(p.ctx, p.includePastResources) + content := gjson.ParseBytes(p.content.MergedBytes) + resources, err := parser.ParseJSON(content, usage) + if err != nil { + log.Fatal(err, "Error parsing ARM template JSON") + } + + for _, res := range resources { + project.PartialResources = append(project.PartialResources, res.PartialResource) + } + +} + +func (p *TemplateProvider) LoadFileContent(filePath string) { + + data, err := os.ReadFile(filePath) + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + + //Store the file content in the content struct + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + log.Fatalf("Failed to unmarshal JSON: %v", err) + } + //If it is not an ARM template, return + if !IsARMTemplate(content) { + return + } + p.content.FileContents[filePath] = content + +} + +func (p *TemplateProvider) MergeFileResources(dirPath string) { + + //If the path is a file, load the file resources + if strings.HasSuffix(dirPath, ".json") { + p.LoadFileContent(dirPath) + return + + } + //If the path is a directory, load all the file resources in the directory that have a .json extension + fileInfos, _ := os.ReadDir(dirPath) + for _, info := range fileInfos { + + if info.IsDir() { + continue + } + + name := info.Name() + filePath := dirPath + "/" + name + + if !strings.HasSuffix(name, ".json") { + continue + } + p.LoadFileContent(filePath) + + } + +} + +func (c *Content) MergeBytes() { + var resources []interface{} + for _, content := range c.FileContents { + resources = append(resources, content.Resources...) + } + + mergedBytes, err := json.Marshal(resources) + if err != nil { + log.Fatalf("Failed to marshal JSON: %v", err) + } + + c.MergedBytes = mergedBytes +} + +func IsARMTemplate(content FileContent) bool { + /* + The schema property is the location of the JavaScript Object Notation (JSON) schema file that describes the version of the template language. + Since it is a required property in an ARM Template, then it will be used to detect whether the file is an ARM Tempalte or not. + + For more information, see: https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/syntax + */ + if content.Schema == "" { + return false + } + + schemaPattern := "https://schema\\.management\\.azure\\.com/schemas/\\d{4}-\\d{2}-\\d{2}/(tenant|managementGroup|subscription)?deploymentTemplate\\.json" + matched, err := regexp.Match(schemaPattern, []byte(content.Schema)) + if err != nil { + return false + } + + // Another way to check if the file is an ARM template is to check if the contentVersion and resources properties are present, since they are required in an ARM template + return matched && content.ContentVersion != "" && content.Resources != nil +} diff --git a/internal/providers/arm/template_provider_test.go b/internal/providers/arm/template_provider_test.go new file mode 100644 index 00000000000..a5f51f65008 --- /dev/null +++ b/internal/providers/arm/template_provider_test.go @@ -0,0 +1,129 @@ +package arm + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/infracost/infracost/internal/config" + "github.com/infracost/infracost/internal/resources/azure" + "github.com/infracost/infracost/internal/schema" + "github.com/sirupsen/logrus" + "gopkg.in/go-playground/assert.v1" +) + +var filePath string = "testdata" + +func TestARMTemplateDetection(t *testing.T) { + expected := map[string]bool{ + "template_valid": true, + "template_invalid_1": false, + } + + for fileName, expectedValue := range expected { + filePath := filepath.Join(filePath, fileName+".json") + data, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + assert.Equal(t, IsARMTemplate(content), expectedValue) + } +} + +func TestDetectInvalidTemplates(t *testing.T) { + expected := 4 + actual := 0 + // Get all files in testdata directory + fileInfos, _ := os.ReadDir(filePath) + for _, info := range fileInfos { + file := info.Name() + data, err := os.ReadFile(filePath + "/" + file) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + + var content FileContent + if err = json.Unmarshal(data, &content); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + if !IsARMTemplate(content) { + actual++ + } + } + assert.Equal(t, actual, expected) +} + +func TestLoadFileContent(t *testing.T) { + + provider := TemplateProvider{ + content: Content{FileContents: map[string]FileContent{}}, + } + + fileInfos, _ := os.ReadDir(filePath) + for _, info := range fileInfos { + if info.IsDir() { + continue + } + name := info.Name() + filePath := filepath.Join(filePath, name) + provider.LoadFileContent(filePath) + } + assert.Equal(t, len(provider.content.FileContents), 4) + +} + +func TestParseFiles(t *testing.T) { + + data := `{ + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + }` + provider := NewTemplateProvider(&config.ProjectContext{ProjectConfig: &config.Project{Path: ""}}, false, "") + project := schema.NewProject("azurerm", &schema.ProjectMetadata{}) + provider.content.MergedBytes = []byte(data) + provider.parseFiles(project, schema.UsageMap{}) + + expected := &azure.ManagedDisk{ + Address: "Microsoft.Compute/disks/ultra", + Region: "francecentral", + ManagedDiskData: azure.ManagedDiskData{ + DiskType: "UltraSSD_LRS", + DiskSizeGB: 2000, + DiskIOPSReadWrite: 4000, + DiskMBPSReadWrite: 20, + }, + } + + assert.Equal(t, project.PartialResources[0].CoreResource, expected) + +} + +func TestLoadResources(t *testing.T) { + ctx := config.NewProjectContext(config.EmptyRunContext(), &config.Project{Path: filePath}, logrus.Fields{}) + provider := NewTemplateProvider(ctx, false, filePath) + projects, err := provider.LoadResources(schema.UsageMap{}) + if err != nil { + t.Fatalf("Failed to load resources: %v", err) + } + assert.Equal(t, len(projects), 1) + assert.Equal(t, len(projects[0].PartialResources), 3) +} diff --git a/internal/providers/arm/testdata/simple_linux_vm.json b/internal/providers/arm/testdata/simple_linux_vm.json new file mode 100644 index 00000000000..5f9adaa1556 --- /dev/null +++ b/internal/providers/arm/testdata/simple_linux_vm.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "4270386830956032562" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-09-01", + "name": "basic_b1", + "location": "francecentral", + "properties": { + "hardwareProfile": { + "vmSize": "standard_b1s" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04-LTSr", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "osProfile": { + "computerName": "standard_b1s", + "adminUsername": "fakeuser", + "adminPassword": "Password1234!" + } + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/standard_disk.json b/internal/providers/arm/testdata/standard_disk.json new file mode 100644 index 00000000000..3ac1e2ebac2 --- /dev/null +++ b/internal/providers/arm/testdata/standard_disk.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "standard", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "Standard_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_1.json b/internal/providers/arm/testdata/template_invalid_1.json new file mode 100644 index 00000000000..865bf8e4ad1 --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_1.json @@ -0,0 +1,12 @@ +{ + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_2.json b/internal/providers/arm/testdata/template_invalid_2.json new file mode 100644 index 00000000000..78e4769bc72 --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_2.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/TestdeploymentTemplate.json#", + + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_3.json b/internal/providers/arm/testdata/template_invalid_3.json new file mode 100644 index 00000000000..0e0071ce7b7 --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_3.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/subscriptionDeploymentTemplate.json#", + + "resources": [] + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_invalid_4.json b/internal/providers/arm/testdata/template_invalid_4.json new file mode 100644 index 00000000000..4e3cc89dd2b --- /dev/null +++ b/internal/providers/arm/testdata/template_invalid_4.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0" + + +} \ No newline at end of file diff --git a/internal/providers/arm/testdata/template_valid.json b/internal/providers/arm/testdata/template_valid.json new file mode 100644 index 00000000000..3960c7092a6 --- /dev/null +++ b/internal/providers/arm/testdata/template_valid.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [] + } \ No newline at end of file diff --git a/internal/providers/arm/testdata/ultra_disk.json b/internal/providers/arm/testdata/ultra_disk.json new file mode 100644 index 00000000000..fb2d0f39573 --- /dev/null +++ b/internal/providers/arm/testdata/ultra_disk.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.26.54.24096", + "templateHash": "7729680081436334184" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2023-10-02", + "name": "ultra", + "location": "francecentral", + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 2000, + "diskIOPSReadWrite": 4000, + "diskMBpsReadWrite": 20 + }, + "sku": { + "name": "UltraSSD_LRS" + } + } + ] + } \ No newline at end of file diff --git a/internal/providers/detect.go b/internal/providers/detect.go index 413c93f07bc..4ff2c5316ec 100644 --- a/internal/providers/detect.go +++ b/internal/providers/detect.go @@ -4,6 +4,7 @@ import ( "archive/zip" "encoding/json" "fmt" + "log" "os" "path/filepath" "sync" @@ -13,6 +14,7 @@ import ( "github.com/infracost/infracost/internal/config" "github.com/infracost/infracost/internal/hcl" "github.com/infracost/infracost/internal/logging" + "github.com/infracost/infracost/internal/providers/arm" "github.com/infracost/infracost/internal/providers/cloudformation" "github.com/infracost/infracost/internal/providers/terraform" "github.com/infracost/infracost/internal/schema" @@ -51,6 +53,8 @@ func Detect(ctx *config.RunContext, project *config.Project, includePastResource return &DetectionOutput{Providers: []schema.Provider{terraform.NewStateJSONProvider(projectContext, includePastResources)}, RootModules: 1}, nil case ProjectTypeCloudFormation: return &DetectionOutput{Providers: []schema.Provider{cloudformation.NewTemplateProvider(projectContext, includePastResources)}, RootModules: 1}, nil + case ProjectTypeARMTemplate: + return &DetectionOutput{Providers: []schema.Provider{arm.NewTemplateProvider(projectContext, includePastResources, project.Path)}}, nil } pathOverrides := make([]hcl.PathOverrideConfig, len(ctx.Config.Autodetect.PathOverrides)) @@ -201,10 +205,12 @@ var ( ProjectTypeTerragruntCLI ProjectType = "terragrunt_cli" ProjectTypeTerraformStateJSON ProjectType = "terraform_state_json" ProjectTypeCloudFormation ProjectType = "cloudformation" + ProjectTypeARMTemplate ProjectType = "arm_template" ProjectTypeAutodetect ProjectType = "autodetect" ) func DetectProjectType(path string, forceCLI bool) ProjectType { + if isCloudFormationTemplate(path) { return ProjectTypeCloudFormation } @@ -220,6 +226,9 @@ func DetectProjectType(path string, forceCLI bool) ProjectType { if isTerraformPlan(path) { return ProjectTypeTerraformPlanBinary } + if IsARMTemplate(path) { + return ProjectTypeARMTemplate + } if forceCLI { if isTerragruntNestedDir(path, 5) { @@ -232,6 +241,21 @@ func DetectProjectType(path string, forceCLI bool) ProjectType { return ProjectTypeAutodetect } +func IsARMTemplate(path string) bool { + data, err := os.ReadFile(path) + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + + //Store the file content in the content struct + var content arm.FileContent + if err = json.Unmarshal(data, &content); err != nil { + log.Fatalf("Failed to unmarshal JSON: %v", err) + } + //If it is not an ARM template, return + return arm.IsARMTemplate(content) +} + func isTerraformPlanJSON(path string) bool { b, err := os.ReadFile(path) if err != nil {