From c0a55f632f560e10a3b6dc8eaaa8ca18e5c00131 Mon Sep 17 00:00:00 2001 From: Arnab <45350738+c0d3r-arnab@users.noreply.github.com> Date: Tue, 28 Sep 2021 18:51:15 +0530 Subject: [PATCH] Add table azure_app_configuration. Closes #308 (#344) --- .../azure_app_configuration/dependencies.txt | 0 .../test-get-expected.json | 10 + .../test-get-query.sql | 3 + .../test-list-expected.json | 10 + .../test-list-query.sql | 3 + .../test-not-found-expected.json | 1 + .../test-not-found-query.sql | 3 + .../test-turbot-expected.json | 10 + .../test-turbot-query.sql | 3 + .../azure_app_configuration/variables.json | 1 + .../azure_app_configuration/variables.tf | 69 ++++ azure/plugin.go | 1 + azure/table_azure_app_configuration.go | 301 ++++++++++++++++++ docs/tables/azure_app_configuration.md | 82 +++++ 14 files changed, 497 insertions(+) create mode 100644 azure-test/tests/azure_app_configuration/dependencies.txt create mode 100644 azure-test/tests/azure_app_configuration/test-get-expected.json create mode 100644 azure-test/tests/azure_app_configuration/test-get-query.sql create mode 100644 azure-test/tests/azure_app_configuration/test-list-expected.json create mode 100644 azure-test/tests/azure_app_configuration/test-list-query.sql create mode 100644 azure-test/tests/azure_app_configuration/test-not-found-expected.json create mode 100644 azure-test/tests/azure_app_configuration/test-not-found-query.sql create mode 100644 azure-test/tests/azure_app_configuration/test-turbot-expected.json create mode 100644 azure-test/tests/azure_app_configuration/test-turbot-query.sql create mode 100644 azure-test/tests/azure_app_configuration/variables.json create mode 100644 azure-test/tests/azure_app_configuration/variables.tf create mode 100644 azure/table_azure_app_configuration.go create mode 100644 docs/tables/azure_app_configuration.md diff --git a/azure-test/tests/azure_app_configuration/dependencies.txt b/azure-test/tests/azure_app_configuration/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_app_configuration/test-get-expected.json b/azure-test/tests/azure_app_configuration/test-get-expected.json new file mode 100644 index 00000000..b11d7f0d --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-get-expected.json @@ -0,0 +1,10 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "region": "{{ output.region.value }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "type": "Microsoft.AppConfiguration/configurationStores" + } +] diff --git a/azure-test/tests/azure_app_configuration/test-get-query.sql b/azure-test/tests/azure_app_configuration/test-get-query.sql new file mode 100644 index 00000000..6c16550b --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region, resource_group, subscription_id +from azure.azure_app_configuration +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_app_configuration/test-list-expected.json b/azure-test/tests/azure_app_configuration/test-list-expected.json new file mode 100644 index 00000000..b11d7f0d --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-list-expected.json @@ -0,0 +1,10 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "region": "{{ output.region.value }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "type": "Microsoft.AppConfiguration/configurationStores" + } +] diff --git a/azure-test/tests/azure_app_configuration/test-list-query.sql b/azure-test/tests/azure_app_configuration/test-list-query.sql new file mode 100644 index 00000000..2dc8bcc9 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-list-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region, resource_group, subscription_id +from azure.azure_app_configuration +where id = '{{ output.resource_id.value }}'; diff --git a/azure-test/tests/azure_app_configuration/test-not-found-expected.json b/azure-test/tests/azure_app_configuration/test-not-found-expected.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-not-found-expected.json @@ -0,0 +1 @@ +null diff --git a/azure-test/tests/azure_app_configuration/test-not-found-query.sql b/azure-test/tests/azure_app_configuration/test-not-found-query.sql new file mode 100644 index 00000000..c852a2d9 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region +from azure.azure_app_configuration +where name = 'dummy-test{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_app_configuration/test-turbot-expected.json b/azure-test/tests/azure_app_configuration/test-turbot-expected.json new file mode 100644 index 00000000..02cd3c76 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-turbot-expected.json @@ -0,0 +1,10 @@ +[ + { + "akas": [ + "{{ output.resource_aka.value }}", + "{{ output.resource_aka_lower.value }}" + ], + "name": "{{ resourceName }}", + "title": "{{ resourceName }}" + } +] diff --git a/azure-test/tests/azure_app_configuration/test-turbot-query.sql b/azure-test/tests/azure_app_configuration/test-turbot-query.sql new file mode 100644 index 00000000..8a3ef53b --- /dev/null +++ b/azure-test/tests/azure_app_configuration/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, akas, title +from azure.azure_app_configuration +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_app_configuration/variables.json b/azure-test/tests/azure_app_configuration/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_app_configuration/variables.tf b/azure-test/tests/azure_app_configuration/variables.tf new file mode 100644 index 00000000..f789d6c3 --- /dev/null +++ b/azure-test/tests/azure_app_configuration/variables.tf @@ -0,0 +1,69 @@ +variable "resource_name" { + type = string + default = "turbot-test-20200927-create-update" + description = "Name of the resource used throughout the test." +} + +variable "azure_environment" { + type = string + default = "public" + description = "Azure environment used for the test." +} + +variable "azure_subscription" { + type = string + default = "3510ae4d-530b-497d-8f30-53c0616fc6c1" + description = "Azure environment used for the test." +} + +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=2.78.0" + } + } +} + +provider "azurerm" { + # Cannot be passed as a variable + features {} + environment = var.azure_environment + subscription_id = var.azure_subscription +} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "West Europe" +} + +resource "azurerm_app_configuration" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + location = azurerm_resource_group.named_test_resource.location +} + +output "region" { + value = azurerm_resource_group.named_test_resource.location +} + +output "resource_aka" { + depends_on = [azurerm_app_configuration.named_test_resource] + value = "azure://${azurerm_app_configuration.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_app_configuration.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_app_configuration.named_test_resource.id +} + +output "subscription_id" { + value = var.azure_subscription +} diff --git a/azure/plugin.go b/azure/plugin.go index 02fddde2..4fdc8561 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -26,6 +26,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_ad_service_principal": tableAzureAdServicePrincipal(ctx), "azure_ad_user": tableAzureAdUser(ctx), "azure_api_management": tableAzureAPIManagement(ctx), + "azure_app_configuration": tableAzureAppConfiguration(ctx), "azure_app_service_environment": tableAzureAppServiceEnvironment(ctx), "azure_app_service_function_app": tableAzureAppServiceFunctionApp(ctx), "azure_app_service_plan": tableAzureAppServicePlan(ctx), diff --git a/azure/table_azure_app_configuration.go b/azure/table_azure_app_configuration.go new file mode 100644 index 00000000..8a98f8ef --- /dev/null +++ b/azure/table_azure_app_configuration.go @@ -0,0 +1,301 @@ +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/monitor/mgmt/insights" + "github.com/Azure/azure-sdk-for-go/services/appconfiguration/mgmt/2020-06-01/appconfiguration" + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" + + "github.com/turbot/steampipe-plugin-sdk/plugin" +) + +//// TABLE DEFINITION + +func tableAzureAppConfiguration(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_app_configuration", + Description: "Azure App Configuration", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"name", "resource_group"}), + Hydrate: getAppConfiguration, + ShouldIgnoreError: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "404"}), + }, + List: &plugin.ListConfig{ + Hydrate: listAppConfigurations, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "The name of the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The resource ID.", + Type: proto.ColumnType_STRING, + Transform: transform.FromGo(), + }, + { + Name: "provisioning_state", + Description: "The provisioning state of the configuration store. Possible values include: 'Creating', 'Updating', 'Deleting', 'Succeeded', 'Failed', 'Canceled'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ConfigurationStoreProperties.ProvisioningState"), + }, + { + Name: "type", + Description: "The type of the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "creation_date", + Description: "The creation date of configuration store.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromField("ConfigurationStoreProperties.CreationDate").Transform(convertDateToTime), + }, + { + Name: "endpoint", + Description: "The DNS endpoint where the configuration store API will be available.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ConfigurationStoreProperties.Endpoint"), + }, + { + Name: "public_network_access", + Description: "Control permission for data plane traffic coming from public networks while private endpoint is enabled. Possible values include: 'Enabled', 'Disabled'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ConfigurationStoreProperties.PublicNetworkAccess"), + }, + { + Name: "sku_name", + Description: "The SKU name of the configuration store.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Name"), + }, + { + Name: "diagnostic_settings", + Description: "A list of active diagnostic settings for the configuration store.", + Type: proto.ColumnType_JSON, + Hydrate: listAppConfigurationDiagnosticSettings, + Transform: transform.FromValue(), + }, + { + Name: "encryption", + Description: "The encryption settings of the configuration store.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ConfigurationStoreProperties.Encryption"), + }, + { + Name: "identity", + Description: "The managed identity information, if configured.", + Type: proto.ColumnType_JSON, + }, + { + Name: "private_endpoint_connections", + Description: "The list of private endpoint connections that are set up for this resource.", + Type: proto.ColumnType_JSON, + Transform: transform.From(extractAppConfigurationPrivateEndpointConnections), + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "tags", + Description: ColumnDescriptionTags, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Tags"), + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ID").Transform(idToAkas), + }, + + // Azure standard columns + { + Name: "region", + Description: ColumnDescriptionRegion, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Location").Transform(toLower), + }, + { + Name: "resource_group", + Description: ColumnDescriptionResourceGroup, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(extractResourceGroupFromID), + }, + { + Name: "subscription_id", + Description: ColumnDescriptionSubscription, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(idToSubscriptionID), + }, + }, + } +} + +//// LIST FUNCTION + +func listAppConfigurations(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + client := appconfiguration.NewConfigurationStoresClient(subscriptionID) + client.Authorizer = session.Authorizer + + result, err := client.List(ctx, "") + if err != nil { + plugin.Logger(ctx).Error("listAppConfigurations", "list", err) + return nil, err + } + + for _, config := range result.Values() { + d.StreamListItem(ctx, config) + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + plugin.Logger(ctx).Error("listAppConfigurations", "list_paging", err) + return nil, err + } + for _, config := range result.Values() { + d.StreamListItem(ctx, config) + } + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getAppConfiguration(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getAppConfiguration") + + name := d.KeyColumnQuals["name"].GetStringValue() + resourceGroup := d.KeyColumnQuals["resource_group"].GetStringValue() + + // Handle empty name or resourceGroup + if name == "" || resourceGroup == "" { + return nil, nil + } + + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + client := appconfiguration.NewConfigurationStoresClient(subscriptionID) + client.Authorizer = session.Authorizer + + config, err := client.Get(ctx, resourceGroup, name) + if err != nil { + plugin.Logger(ctx).Error("getAppConfiguration", "get", err) + return nil, err + } + + // In some cases resource does not give any notFound error + // instead of notFound error, it returns empty data + if config.ID != nil { + return config, nil + } + + return nil, nil +} + +func listAppConfigurationDiagnosticSettings(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listAppConfigurationDiagnosticSettings") + id := *h.Item.(appconfiguration.ConfigurationStore).ID + + // Create session + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + client := insights.NewDiagnosticSettingsClient(subscriptionID) + client.Authorizer = session.Authorizer + + op, err := client.List(ctx, id) + if err != nil { + plugin.Logger(ctx).Error("listAppConfigurationDiagnosticSettings", "list", err) + return nil, err + } + + // If we return the API response directly, the output does not provide all + // the contents of DiagnosticSettings + var diagnosticSettings []map[string]interface{} + for _, i := range *op.Value { + objectMap := make(map[string]interface{}) + if i.ID != nil { + objectMap["id"] = i.ID + } + if i.Name != nil { + objectMap["name"] = i.Name + } + if i.Type != nil { + objectMap["type"] = i.Type + } + if i.DiagnosticSettings != nil { + objectMap["properties"] = i.DiagnosticSettings + } + diagnosticSettings = append(diagnosticSettings, objectMap) + } + return diagnosticSettings, nil +} + +// //// TRANSFORM FUNCTION + +// If we return the API response directly, the output will not provide all the properties of PrivateEndpointConnections +func extractAppConfigurationPrivateEndpointConnections(ctx context.Context, d *transform.TransformData) (interface{}, error) { + server := d.HydrateItem.(appconfiguration.ConfigurationStore) + var properties []map[string]interface{} + + if server.ConfigurationStoreProperties.PrivateEndpointConnections != nil { + for _, i := range *server.ConfigurationStoreProperties.PrivateEndpointConnections { + objectMap := make(map[string]interface{}) + if i.ID != nil { + objectMap["id"] = i.ID + } + if i.ID != nil { + objectMap["name"] = i.Name + } + if i.ID != nil { + objectMap["type"] = i.Type + } + if i.PrivateEndpointConnectionProperties != nil { + if i.PrivateEndpointConnectionProperties.PrivateEndpoint != nil { + objectMap["privateEndpointPropertyId"] = i.PrivateEndpointConnectionProperties.PrivateEndpoint.ID + } + if i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState != nil { + if len(i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.ActionsRequired) > 0 { + objectMap["privateLinkServiceConnectionStateActionsRequired"] = i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.ActionsRequired + } + if len(i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.Status) > 0 { + objectMap["privateLinkServiceConnectionStateStatus"] = i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.Status + } + if i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.Description != nil { + objectMap["privateLinkServiceConnectionStateDescription"] = i.PrivateEndpointConnectionProperties.PrivateLinkServiceConnectionState.Description + } + } + if len(i.PrivateEndpointConnectionProperties.ProvisioningState) > 0 { + objectMap["provisioningState"] = i.PrivateEndpointConnectionProperties.ProvisioningState + } + } + properties = append(properties, objectMap) + } + } + + return properties, nil +} diff --git a/docs/tables/azure_app_configuration.md b/docs/tables/azure_app_configuration.md new file mode 100644 index 00000000..b205d983 --- /dev/null +++ b/docs/tables/azure_app_configuration.md @@ -0,0 +1,82 @@ +# Table: azure_app_configuration + +Azure App Configuration provides a service to centrally manage application settings and feature flags. App Configuration is used to store all the settings for your application and secure their accesses in one place. + +## Examples + +### Basic info + +```sql +select + id, + name, + type, + provisioning_state, + creation_date +from + azure_app_configuration; +``` + +### List public network access enabled app configurations + +```sql +select + id, + name, + type, + provisioning_state, + public_network_access +from + azure_app_configuration +where + public_network_access = 'Enabled'; +``` + +### List app configurations with user assigned identities + +```sql +select + id, + name, + identity -> 'type' as identity_type, + jsonb_pretty(identity -> 'userAssignedIdentities') as identity_user_assigned_identities +from + azure_app_configuration +where + exists ( + select + from + unnest(regexp_split_to_array(identity ->> 'type', ',')) elem + where + trim(elem) = 'UserAssigned' + ); +``` + +### List private endpoint connection details for app configurations + +```sql +select + name as app_config_name, + id as app_config_id, + connections ->> 'id' as connection_id, + connections ->> 'privateEndpointPropertyId' as connection_private_endpoint_property_id, + connections ->> 'privateLinkServiceConnectionStateActionsRequired' as connection_actions_required, + connections ->> 'privateLinkServiceConnectionStateDescription' as connection_description, + connections ->> 'privateLinkServiceConnectionStateStatus' as connection_status, + connections ->> 'provisioningState' as connection_provisioning_state +from + azure_app_configuration, + jsonb_array_elements(private_endpoint_connections) as connections; +``` + +### List encryption details for app configurations + +```sql +select + name as app_config_name, + id as app_config_id, + encryption -> 'keyVaultProperties' ->> 'identityClientId' as key_vault_identity_client_id, + encryption -> 'keyVaultProperties' ->> 'keyIdentifier' as key_vault_key_identifier +from + azure_app_configuration; +```