diff --git a/azure-test/tests/azure_lb/dependencies.txt b/azure-test/tests/azure_lb/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_lb/test-get-expected.json b/azure-test/tests/azure_lb/test-get-expected.json new file mode 100644 index 00000000..007fa65d --- /dev/null +++ b/azure-test/tests/azure_lb/test-get-expected.json @@ -0,0 +1,13 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "region": "{{ output.region.value }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "tags": { + "name": "{{ resourceName }}" + }, + "type": "Microsoft.Network/loadBalancers" + } +] diff --git a/azure-test/tests/azure_lb/test-get-query.sql b/azure-test/tests/azure_lb/test-get-query.sql new file mode 100644 index 00000000..1c7f5486 --- /dev/null +++ b/azure-test/tests/azure_lb/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region, resource_group, subscription_id, tags +from azure.azure_lb +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb/test-list-expected.json b/azure-test/tests/azure_lb/test-list-expected.json new file mode 100644 index 00000000..f2d090cd --- /dev/null +++ b/azure-test/tests/azure_lb/test-list-expected.json @@ -0,0 +1,6 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_lb/test-list-query.sql b/azure-test/tests/azure_lb/test-list-query.sql new file mode 100644 index 00000000..8896b509 --- /dev/null +++ b/azure-test/tests/azure_lb/test-list-query.sql @@ -0,0 +1,3 @@ +select name, id +from azure.azure_lb +where name = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb/test-not-found-expected.json b/azure-test/tests/azure_lb/test-not-found-expected.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/azure-test/tests/azure_lb/test-not-found-expected.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/azure-test/tests/azure_lb/test-not-found-query.sql b/azure-test/tests/azure_lb/test-not-found-query.sql new file mode 100644 index 00000000..83137a94 --- /dev/null +++ b/azure-test/tests/azure_lb/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, id, type, region +from azure.azure_lb +where name = 'dummy-test{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb/test-turbot-expected.json b/azure-test/tests/azure_lb/test-turbot-expected.json new file mode 100644 index 00000000..bfea6a83 --- /dev/null +++ b/azure-test/tests/azure_lb/test-turbot-expected.json @@ -0,0 +1,13 @@ +[ + { + "akas": [ + "{{ output.resource_aka.value }}", + "{{ output.resource_aka_lower.value }}" + ], + "name": "{{ resourceName }}", + "tags": { + "name": "{{ resourceName }}" + }, + "title": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_lb/test-turbot-query.sql b/azure-test/tests/azure_lb/test-turbot-query.sql new file mode 100644 index 00000000..e7e74e64 --- /dev/null +++ b/azure-test/tests/azure_lb/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, akas, tags, title +from azure.azure_lb +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb/variables.json b/azure-test/tests/azure_lb/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_lb/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_lb/variables.tf b/azure-test/tests/azure_lb/variables.tf new file mode 100644 index 00000000..509f6c05 --- /dev/null +++ b/azure-test/tests/azure_lb/variables.tf @@ -0,0 +1,65 @@ +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-53c0616fc6c1" + description = "Azure environment used for the test." +} + +provider "azurerm" { + # Cannot be passed as a variable + version = "=1.36.0" + environment = var.azure_environment + subscription_id = var.azure_subscription +} + +data "azuread_client_config" "current" {} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "West Europe" +} + +resource "azurerm_lb" "named_test_resource" { + name = var.resource_name + location = azurerm_resource_group.named_test_resource.location + resource_group_name = azurerm_resource_group.named_test_resource.name + tags = { + name = var.resource_name + } +} + +output "region" { + value = azurerm_resource_group.named_test_resource.location +} + +output "resource_aka" { + depends_on = [azurerm_lb.named_test_resource] + value = "azure://${azurerm_lb.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_lb.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_lb.named_test_resource.id +} + +output "subscription_id" { + value = var.azure_subscription +} diff --git a/azure/plugin.go b/azure/plugin.go index 353a6bc0..0a59d99f 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -66,6 +66,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_key_vault_managed_hardware_security_module": tableAzureKeyVaultManagedHardwareSecurityModule(ctx), "azure_key_vault_secret": tableAzureKeyVaultSecret(ctx), "azure_kubernetes_cluster": tableAzureKubernetesCluster(ctx), + "azure_lb": tableAzureLoadBalancer(ctx), "azure_location": tableAzureLocation(ctx), "azure_log_alert": tableAzureLogAlert(ctx), "azure_log_profile": tableAzureLogProfile(ctx), diff --git a/azure/table_azure_lb.go b/azure/table_azure_lb.go new file mode 100644 index 00000000..6aa542d4 --- /dev/null +++ b/azure/table_azure_lb.go @@ -0,0 +1,287 @@ +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/network/mgmt/2021-02-01/network" + "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 tableAzureLoadBalancer(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_lb", + Description: "Azure Load Balancer", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"name", "resource_group"}), + Hydrate: getLoadBalancer, + ShouldIgnoreError: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "404"}), + }, + List: &plugin.ListConfig{ + Hydrate: listLoadBalancers, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "The resource name.", + 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 load balancer resource. Possible values include: 'Succeeded', 'Updating', 'Deleting', 'Failed'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("LoadBalancerPropertiesFormat.ProvisioningState"), + }, + { + Name: "type", + Description: "The resource type.", + Type: proto.ColumnType_STRING, + }, + { + Name: "etag", + Description: "A unique read-only string that changes whenever the resource is updated.", + Type: proto.ColumnType_STRING, + }, + { + Name: "extended_location_name", + Description: "The name of the extended location.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ExtendedLocation.Name"), + }, + { + Name: "extended_location_type", + Description: "The type of the extended location. Possible values include: 'ExtendedLocationTypesEdgeZone'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ExtendedLocation.Type").Transform(transform.ToString), + }, + { + Name: "resource_guid", + Description: "The resource GUID property of the load balancer resource.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("LoadBalancerPropertiesFormat.ResourceGUID"), + }, + { + Name: "sku_name", + Description: "Name of the load balancer SKU. Possible values include: 'Basic', 'Standard', 'Gateway'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Name").Transform(transform.ToString), + }, + { + Name: "sku_tier", + Description: "Tier of the load balancer SKU. Possible values include: 'Regional', 'Global'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Sku.Tier").Transform(transform.ToString), + }, + { + Name: "backend_address_pools", + Description: "Collection of backend address pools used by the load balancer.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.BackendAddressPools"), + }, + { + Name: "diagnostic_settings", + Description: "A list of active diagnostic settings for the load balancer.", + Type: proto.ColumnType_JSON, + Hydrate: listLoadBalancerDiagnosticSettings, + Transform: transform.FromValue(), + }, + { + Name: "frontend_ip_configurations", + Description: "Object representing the frontend IPs to be used for the load balancer.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.FrontendIPConfigurations"), + }, + { + Name: "inbound_nat_pools", + Description: "Defines an external port range for inbound NAT to a single backend port on NICs associated with the load balancer. Inbound NAT rules are created automatically for each NIC associated with the Load Balancer using an external port from this range. Defining an Inbound NAT pool on the Load Balancer is mutually exclusive with defining inbound Nat rules. Inbound NAT pools are referenced from virtual machine scale sets. NICs that are associated with individual virtual machines cannot reference an inbound NAT pool. They have to reference individual inbound NAT rules.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.InboundNatPools"), + }, + { + Name: "inbound_nat_rules", + Description: "Collection of inbound NAT Rules used by the load balancer. Defining inbound NAT rules on the load balancer is mutually exclusive with defining an inbound NAT pool. Inbound NAT pools are referenced from virtual machine scale sets. NICs that are associated with individual virtual machines cannot reference an Inbound NAT pool. They have to reference individual inbound NAT rules.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.InboundNatRules"), + }, + { + Name: "load_balancing_rules", + Description: "Object collection representing the load balancing rules Gets the provisioning.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.LoadBalancingRules"), + }, + { + Name: "outbound_rules", + Description: "The outbound rules.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.OutboundRules"), + }, + { + Name: "probes", + Description: "Collection of probe objects used in the load balancer.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("LoadBalancerPropertiesFormat.Probes"), + }, + + // 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), + }, + { + Name: "subscription_id", + Description: ColumnDescriptionSubscription, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ID").Transform(idToSubscriptionID), + }, + }, + } +} + +//// LIST FUNCTIONS + +func listLoadBalancers(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 + + LoadBalancersClient := network.NewLoadBalancersClient(subscriptionID) + LoadBalancersClient.Authorizer = session.Authorizer + + result, err := LoadBalancersClient.ListAll(ctx) + if err != nil { + return nil, err + } + + for _, loadBalancer := range result.Values() { + d.StreamListItem(ctx, loadBalancer) + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + return nil, err + } + for _, loadBalancer := range result.Values() { + d.StreamListItem(ctx, loadBalancer) + } + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getLoadBalancer(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getLoadBalancer") + + 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 + + LoadBalancersClient := network.NewLoadBalancersClient(subscriptionID) + LoadBalancersClient.Authorizer = session.Authorizer + + op, err := LoadBalancersClient.Get(ctx, resourceGroup, name, "") + if err != nil { + return nil, err + } + + // In some cases resource does not give any notFound error + // instead of notFound error, it returns empty data + if op.ID != nil { + return op, nil + } + + return nil, nil +} + +func listLoadBalancerDiagnosticSettings(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("listAzureLoadBalancerDiagnosticSettings") + id := *h.Item.(network.LoadBalancer).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_lb.md b/docs/tables/azure_lb.md new file mode 100644 index 00000000..99e826e8 --- /dev/null +++ b/docs/tables/azure_lb.md @@ -0,0 +1,33 @@ +# Table: azure_lb + +Azure Load Balancer operates at layer 4 of the Open Systems Interconnection (OSI) model. It's the single point of contact for clients. Load balancer distributes inbound flows that arrive at the load balancer's front end to backend pool instances. These flows are according to configured load-balancing rules and health probes. The backend pool instances can be Azure Virtual Machines or instances in a virtual machine scale set. + +## Examples + +### Basic info + +```sql +select + id, + name, + type, + provisioning_state, + etag, + region +from + azure_lb; +``` + +### List failed load balancers + +```sql +select + id, + name, + type, + provisioning_state +from + azure_lb +where + provisioning_state = 'Failed'; +``` \ No newline at end of file