diff --git a/azure-test/tests/azure_search_service/dependencies.txt b/azure-test/tests/azure_search_service/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_search_service/test-get-expected.json b/azure-test/tests/azure_search_service/test-get-expected.json new file mode 100644 index 00000000..210210f2 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-get-expected.json @@ -0,0 +1,13 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "provisioning_state": "succeeded", + "region": "east us", + "resource_group": "{{ resourceName }}", + "sku_name": "standard", + "status": "running", + "subscription_id": "{{ output.subscription_id.value }}", + "type": "Microsoft.Search/searchServices" + } +] diff --git a/azure-test/tests/azure_search_service/test-get-query.sql b/azure-test/tests/azure_search_service/test-get-query.sql new file mode 100644 index 00000000..666def8f --- /dev/null +++ b/azure-test/tests/azure_search_service/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id, type, status, provisioning_state, sku_name, resource_group, region, subscription_id +from azure.azure_search_service +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_search_service/test-hydrate-expected.json b/azure-test/tests/azure_search_service/test-hydrate-expected.json new file mode 100644 index 00000000..2581c5bb --- /dev/null +++ b/azure-test/tests/azure_search_service/test-hydrate-expected.json @@ -0,0 +1,7 @@ +[ + { + "name": "{{ resourceName }}", + "partition_count": 1, + "replica_count": 1 + } +] diff --git a/azure-test/tests/azure_search_service/test-hydrate-query.sql b/azure-test/tests/azure_search_service/test-hydrate-query.sql new file mode 100644 index 00000000..5224d3b5 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-hydrate-query.sql @@ -0,0 +1,3 @@ +select name, replica_count, partition_count +from azure.azure_search_service +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; \ No newline at end of file diff --git a/azure-test/tests/azure_search_service/test-list-expected.json b/azure-test/tests/azure_search_service/test-list-expected.json new file mode 100644 index 00000000..7d108981 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-list-expected.json @@ -0,0 +1,7 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "region": "east us" + } +] diff --git a/azure-test/tests/azure_search_service/test-list-query.sql b/azure-test/tests/azure_search_service/test-list-query.sql new file mode 100644 index 00000000..a2444e07 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-list-query.sql @@ -0,0 +1,3 @@ +select id, name, region +from azure.azure_search_service +where name = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_search_service/test-not-found-expected.json b/azure-test/tests/azure_search_service/test-not-found-expected.json new file mode 100644 index 00000000..19765bd5 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-not-found-expected.json @@ -0,0 +1 @@ +null diff --git a/azure-test/tests/azure_search_service/test-not-found-query.sql b/azure-test/tests/azure_search_service/test-not-found-query.sql new file mode 100644 index 00000000..fe460c4a --- /dev/null +++ b/azure-test/tests/azure_search_service/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, akas, title +from azure.azure_search_service +where name = 'dummy-{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_search_service/test-turbot-expected.json b/azure-test/tests/azure_search_service/test-turbot-expected.json new file mode 100644 index 00000000..02cd3c76 --- /dev/null +++ b/azure-test/tests/azure_search_service/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_search_service/test-turbot-query.sql b/azure-test/tests/azure_search_service/test-turbot-query.sql new file mode 100644 index 00000000..35f9b5d9 --- /dev/null +++ b/azure-test/tests/azure_search_service/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, akas, title +from azure.azure_search_service +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_search_service/variables.json b/azure-test/tests/azure_search_service/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_search_service/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_search_service/variables.tf b/azure-test/tests/azure_search_service/variables.tf new file mode 100644 index 00000000..f84fa594 --- /dev/null +++ b/azure-test/tests/azure_search_service/variables.tf @@ -0,0 +1,69 @@ +variable "resource_name" { + type = string + default = "turbot-test-20200125-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-53b9616fc6c1" + description = "Azure subscription used for the test." +} + +provider "azurerm" { + # Cannot be passed as a variable + version = "=2.50.0" + features {} + environment = var.azure_environment + subscription_id = var.azure_subscription +} + +data "azurerm_client_config" "current" {} + +data "null_data_source" "resource" { + inputs = { + scope = "azure:///subscriptions/${data.azurerm_client_config.current.subscription_id}" + } +} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "East US" +} + +resource "azurerm_search_service" "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 + sku = "standard" +} + +output "resource_aka" { + value = "azure://${azurerm_search_service.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_search_service.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_search_service.named_test_resource.id +} + +output "location" { + value = lower(azurerm_search_service.named_test_resource.location) +} + +output "subscription_id" { + value = var.azure_subscription +} diff --git a/azure/plugin.go b/azure/plugin.go index 78e35882..fe43ca5f 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -88,6 +88,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_role_assignment": tableAzureIamRoleAssignment(ctx), "azure_role_definition": tableAzureIamRoleDefinition(ctx), "azure_route_table": tableAzureRouteTable(ctx), + "azure_search_service": tableAzureSearchService(ctx), "azure_security_center_auto_provisioning": tableAzureSecurityCenterAutoProvisioning(ctx), "azure_security_center_contact": tableAzureSecurityCenterContact(ctx), "azure_security_center_jit_network_access_policy": tableAzureSecurityCenterJITNetworkAccessPolicy(ctx), diff --git a/azure/table_azure_search_service.go b/azure/table_azure_search_service.go new file mode 100644 index 00000000..9667b38a --- /dev/null +++ b/azure/table_azure_search_service.go @@ -0,0 +1,270 @@ +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/search/mgmt/2020-08-01/search" + "github.com/turbot/steampipe-plugin-sdk/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/plugin" + "github.com/turbot/steampipe-plugin-sdk/plugin/transform" +) + +//// TABLE DEFINITION + +func tableAzureSearchService(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_search_service", + Description: "Azure Search Service", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"name", "resource_group"}), + Hydrate: getSearchService, + ShouldIgnoreError: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "404"}), + }, + List: &plugin.ListConfig{ + Hydrate: listSearchServices, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Type: proto.ColumnType_STRING, + Description: "The name of the resource.", + }, + { + Name: "id", + Type: proto.ColumnType_STRING, + Description: "Fully qualified resource ID for the resource.", + Transform: transform.FromGo(), + }, + { + Name: "provisioning_state", + Type: proto.ColumnType_STRING, + Description: "The state of the last provisioning operation performed on the search service.", + Transform: transform.FromField("ServiceProperties.ProvisioningState"), + }, + { + Name: "status", + Type: proto.ColumnType_STRING, + Description: "The status of the search service. Possible values include: 'running', deleting', 'provisioning', 'degraded', 'disabled', 'error' etc.", + Transform: transform.FromField("ServiceProperties.Status"), + }, + { + Name: "status_details", + Type: proto.ColumnType_STRING, + Description: "The details of the search service status.", + Transform: transform.FromField("ServiceProperties.StatusDetails"), + }, + { + Name: "type", + Type: proto.ColumnType_STRING, + Description: "The type of the resource.", + }, + { + Name: "hosting_mode", + Type: proto.ColumnType_STRING, + Description: "Applicable only for the standard3 SKU. You can set this property to enable up to 3 high density partitions that allow up to 1000 indexes, which is much higher than the maximum indexes allowed for any other SKU. For the standard3 SKU, the value is either 'default' or 'highDensity'. For all other SKUs, this value must be 'default'. Possible values include: 'Default', 'HighDensity'.", + Transform: transform.FromField("ServiceProperties.HostingMode"), + }, + { + Name: "partition_count", + Type: proto.ColumnType_INT, + Description: "The number of partitions in the search service; if specified, it can be 1, 2, 3, 4, 6, or 12. Values greater than 1 are only valid for standard SKUs. For 'standard3' services with hostingMode set to 'highDensity', the allowed values are between 1 and 3.", + Transform: transform.FromField("ServiceProperties.PartitionCount"), + }, + { + Name: "public_network_access", + Type: proto.ColumnType_STRING, + Description: "This value can be set to 'enabled' to avoid breaking changes on existing customer resources and templates. If set to 'disabled', traffic over public interface is not allowed, and private endpoint connections would be the exclusive access method. Possible values include: 'Enabled', 'Disabled'.", + Transform: transform.FromField("ServiceProperties.PublicNetworkAccess"), + }, + { + Name: "replica_count", + Type: proto.ColumnType_INT, + Description: "The number of replicas in the search service. If specified, it must be a value between 1 and 12 inclusive for standard SKUs or between 1 and 3 inclusive for basic SKU.", + Transform: transform.FromField("ServiceProperties.ReplicaCount"), + }, + { + Name: "sku_name", + Type: proto.ColumnType_STRING, + Description: "The SKU of the Search Service, which determines price tier and capacity limits. This property is required when creating a new search service.", + Transform: transform.FromField("Sku.Name"), + }, + { + Name: "diagnostic_settings", + Description: "A list of active diagnostic settings for the search service.", + Type: proto.ColumnType_JSON, + Hydrate: listSearchServiceDiagnosticSettings, + Transform: transform.FromValue(), + }, + { + Name: "identity", + Type: proto.ColumnType_JSON, + Description: "The identity of the resource.", + }, + { + Name: "network_rule_set", + Type: proto.ColumnType_JSON, + Description: "Network specific rules that determine how the azure cognitive search service may be reached.", + Transform: transform.FromField("ServiceProperties.NetworkRuleSet"), + }, + { + Name: "private_endpoint_connections", + Type: proto.ColumnType_JSON, + Description: "The list of private endpoint connections to the azure cognitive search service.", + Transform: transform.FromField("ServiceProperties.PrivateEndpointConnections"), + }, + { + Name: "shared_private_link_resources", + Type: proto.ColumnType_JSON, + Description: "The list of shared private link resources managed by the azure cognitive search service.", + Transform: transform.FromField("ServiceProperties.SharedPrivateLinkResources"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "tags", + Description: ColumnDescriptionTags, + Type: proto.ColumnType_JSON, + }, + { + 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).Transform(toLower), + }, + { + Name: "subscription_id", + Description: ColumnDescriptionSubscription, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(idToSubscriptionID), + }, + }, + } +} + +//// LIST FUNCTION + +func listSearchServices(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, nil + } + subscriptionID := session.SubscriptionID + + searchClient := search.NewServicesClient(subscriptionID) + searchClient.Authorizer = session.Authorizer + + result, err := searchClient.ListBySubscription(ctx, nil) + if err != nil { + return nil, err + } + for _, service := range result.Values() { + d.StreamListItem(ctx, service) + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + return nil, err + } + for _, service := range result.Values() { + d.StreamListItem(ctx, service) + } + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getSearchService(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getSearchService") + + name := d.KeyColumnQuals["name"].GetStringValue() + resourceGroup := d.KeyColumnQuals["resource_group"].GetStringValue() + + if name == "" || resourceGroup == "" { + return nil, nil + } + + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + searchClient := search.NewServicesClient(subscriptionID) + searchClient.Authorizer = session.Authorizer + + op, err := searchClient.Get(ctx, resourceGroup, name, nil) + if err != nil { + return nil, err + } + + if op.ID != nil { + return op, nil + } + + return nil, nil +} + +func listSearchServiceDiagnosticSettings(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listSearchServiceDiagnosticSettings") + id := h.Item.(search.Service).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 { + return nil, err + } + + // If we return the API response directly, the output only gives + // 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 +} diff --git a/docs/tables/azure_search_service.md b/docs/tables/azure_search_service.md new file mode 100644 index 00000000..16453f6f --- /dev/null +++ b/docs/tables/azure_search_service.md @@ -0,0 +1,33 @@ +# Table: azure_search_service + +Azure Cognitive Search is the only cloud search service with built-in AI capabilities that enrich all types of information to help you identify and explore relevant content at scale. Use cognitive skills for vision, language and speech or use custom machine learning models to uncover insights from all types of content. + +## Examples + +### Basic info + +```sql +select + name, + id, + type, + provisioning_state, + status, + sku_name, + replica_count +from + azure_search_service; +``` + +### List publicly accessible search services + +```sql +select + name, + id, + public_network_access +from + azure_search_service +where + public_network_access = 'Enabled'; +``` \ No newline at end of file