diff --git a/azure-test/tests/azure_lb_probe/dependencies.txt b/azure-test/tests/azure_lb_probe/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_lb_probe/test-get-expected.json b/azure-test/tests/azure_lb_probe/test-get-expected.json new file mode 100644 index 00000000..60304b13 --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-get-expected.json @@ -0,0 +1,9 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "type": "Microsoft.Network/loadBalancers/probes" + } +] diff --git a/azure-test/tests/azure_lb_probe/test-get-query.sql b/azure-test/tests/azure_lb_probe/test-get-query.sql new file mode 100644 index 00000000..c7c4e53b --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id, type, resource_group, subscription_id +from azure.azure_lb_probe +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb_probe/test-list-expected.json b/azure-test/tests/azure_lb_probe/test-list-expected.json new file mode 100644 index 00000000..f2d090cd --- /dev/null +++ b/azure-test/tests/azure_lb_probe/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_probe/test-list-query.sql b/azure-test/tests/azure_lb_probe/test-list-query.sql new file mode 100644 index 00000000..185ae304 --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-list-query.sql @@ -0,0 +1,3 @@ +select name, id +from azure.azure_lb_probe +where name = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb_probe/test-not-found-expected.json b/azure-test/tests/azure_lb_probe/test-not-found-expected.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-not-found-expected.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/azure-test/tests/azure_lb_probe/test-not-found-query.sql b/azure-test/tests/azure_lb_probe/test-not-found-query.sql new file mode 100644 index 00000000..01cedfca --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, id +from azure.azure_lb_probe +where name = 'dummy-test-{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb_probe/test-turbot-expected.json b/azure-test/tests/azure_lb_probe/test-turbot-expected.json new file mode 100644 index 00000000..b6272600 --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-turbot-expected.json @@ -0,0 +1,10 @@ +[ + { + "akas": [ + "{{ output.resource_aka.value }}", + "{{ output.resource_aka_lower.value }}" + ], + "name": "{{ resourceName }}", + "title": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_lb_probe/test-turbot-query.sql b/azure-test/tests/azure_lb_probe/test-turbot-query.sql new file mode 100644 index 00000000..ccc51d3c --- /dev/null +++ b/azure-test/tests/azure_lb_probe/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, akas, title +from azure.azure_lb_probe +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_lb_probe/variables.json b/azure-test/tests/azure_lb_probe/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_lb_probe/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_lb_probe/variables.tf b/azure-test/tests/azure_lb_probe/variables.tf new file mode 100644 index 00000000..0acf118c --- /dev/null +++ b/azure-test/tests/azure_lb_probe/variables.tf @@ -0,0 +1,77 @@ +variable "resource_name" { + type = string + default = "turbot-test-azure-lb-probe-20210806" + 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 US" +} + +resource "azurerm_public_ip" "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 + allocation_method = "Static" +} + +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 + + frontend_ip_configuration { + name = var.resource_name + public_ip_address_id = azurerm_public_ip.named_test_resource.id + } +} + +resource "azurerm_lb_probe" "named_test_resource" { + resource_group_name = azurerm_resource_group.named_test_resource.name + loadbalancer_id = azurerm_lb.named_test_resource.id + name = var.resource_name + port = 22 +} + +output "resource_aka" { + depends_on = [azurerm_lb_probe.named_test_resource] + value = "azure://${azurerm_lb_probe.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_lb_probe.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_lb_probe.named_test_resource.id +} + +output "subscription_id" { + value = var.azure_subscription +} diff --git a/azure/plugin.go b/azure/plugin.go index f38b9d99..b83373f4 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -69,6 +69,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_key_vault_secret": tableAzureKeyVaultSecret(ctx), "azure_kubernetes_cluster": tableAzureKubernetesCluster(ctx), "azure_lb": tableAzureLoadBalancer(ctx), + "azure_lb_probe": tableAzureLoadBalancerProbe(ctx), "azure_lb_rule": tableAzureLoadBalancerRule(ctx), "azure_location": tableAzureLocation(ctx), "azure_log_alert": tableAzureLogAlert(ctx), diff --git a/azure/table_azure_lb_probe.go b/azure/table_azure_lb_probe.go new file mode 100644 index 00000000..02e625cb --- /dev/null +++ b/azure/table_azure_lb_probe.go @@ -0,0 +1,212 @@ +package azure + +import ( + "context" + "strings" + + "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 tableAzureLoadBalancerProbe(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_lb_probe", + Description: "Azure Load Balancer Probe", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"load_balancer_name", "name", "resource_group"}), + Hydrate: getLoadBalancerProbe, + ShouldIgnoreError: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "404"}), + }, + List: &plugin.ListConfig{ + Hydrate: listLoadBalancerProbes, + ParentHydrate: listLoadBalancers, + }, + Columns: []*plugin.Column{ + { + Name: "name", + Description: "The name of the resource that is unique within the set of probes used by the load balancer. This name can be used to access the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "id", + Description: "The resource ID.", + Type: proto.ColumnType_STRING, + Transform: transform.FromGo(), + }, + { + Name: "load_balancer_name", + Description: "The friendly name that identifies the load balancer.", + Type: proto.ColumnType_STRING, + Transform: transform.From(extractLoadBalancerNameFromProbeID), + }, + { + Name: "provisioning_state", + Description: "The provisioning state of the probe resource. Possible values include: 'Succeeded', 'Updating', 'Deleting', 'Failed'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ProbePropertiesFormat.ProvisioningState"), + }, + { + Name: "type", + Description: "Type of the resource.", + Type: proto.ColumnType_STRING, + }, + { + Name: "etag", + Description: "A unique read-only string that changes whenever the resource is updated.", + Type: proto.ColumnType_STRING, + }, + { + Name: "interval_in_seconds", + Description: "The interval, in seconds, for how frequently to probe the endpoint for health status. Typically, the interval is slightly less than half the allocated timeout period (in seconds) which allows two full probes before taking the instance out of rotation. The default value is 15, the minimum value is 5.", + Type: proto.ColumnType_INT, + Transform: transform.FromField("ProbePropertiesFormat.IntervalInSeconds"), + }, + { + Name: "number_of_probes", + Description: "The number of probes where if no response, will result in stopping further traffic from being delivered to the endpoint. This values allows endpoints to be taken out of rotation faster or slower than the typical times used in Azure.", + Type: proto.ColumnType_INT, + Transform: transform.FromField("ProbePropertiesFormat.NumberOfProbes"), + }, + { + Name: "port", + Description: "The port for communicating the probe. Possible values range from 1 to 65535, inclusive.", + Type: proto.ColumnType_INT, + Transform: transform.FromField("ProbePropertiesFormat.Port"), + }, + { + Name: "protocol", + Description: "The protocol of the end point. If 'Tcp' is specified, a received ACK is required for the probe to be successful. If 'Http' or 'Https' is specified, a 200 OK response from the specifies URI is required for the probe to be successful. Possible values include: 'HTTP', 'TCP', 'HTTPS'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ProbePropertiesFormat.Protocol"), + }, + { + Name: "request_path", + Description: "The URI used for requesting health status from the VM. Path is required if a protocol is set to http. Otherwise, it is not allowed. There is no default value.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("ProbePropertiesFormat.RequestPath"), + }, + { + Name: "load_balancing_rules", + Description: "The load balancer rules that use this probe.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ProbePropertiesFormat.LoadBalancingRules"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Name"), + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ID").Transform(idToAkas), + }, + + // Azure standard columns + { + 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 listLoadBalancerProbes(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + // Get the details of load balancer + loadBalancer := h.Item.(network.LoadBalancer) + + // Create session + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + resourceGroup := strings.Split(*loadBalancer.ID, "/")[4] + + listLoadBalancerProbesClient := network.NewLoadBalancerProbesClient(subscriptionID) + listLoadBalancerProbesClient.Authorizer = session.Authorizer + + result, err := listLoadBalancerProbesClient.List(ctx, resourceGroup, *loadBalancer.Name) + if err != nil { + return nil, err + } + for _, probe := range result.Values() { + d.StreamListItem(ctx, probe) + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + return nil, err + } + for _, probe := range result.Values() { + d.StreamListItem(ctx, probe) + } + } + + return nil, err +} + +//// HYDRATE FUNCTION + +func getLoadBalancerProbe(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + plugin.Logger(ctx).Trace("getLoadBalancerProbe") + + loadBalancerName := d.KeyColumnQuals["load_balancer_name"].GetStringValue() + probeName := d.KeyColumnQuals["name"].GetStringValue() + resourceGroup := d.KeyColumnQuals["resource_group"].GetStringValue() + + // Handle empty loadBalancerName, probeName or resourceGroup + if loadBalancerName == "" || probeName == "" || resourceGroup == "" { + return nil, nil + } + + session, err := GetNewSession(ctx, d, "MANAGEMENT") + if err != nil { + return nil, err + } + subscriptionID := session.SubscriptionID + + LoadBalancerProbeClient := network.NewLoadBalancerProbesClient(subscriptionID) + LoadBalancerProbeClient.Authorizer = session.Authorizer + + op, err := LoadBalancerProbeClient.Get(ctx, resourceGroup, loadBalancerName, probeName) + 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 +} + +//// TRANSFORM FUNCTION + +func extractLoadBalancerNameFromProbeID(ctx context.Context, d *transform.TransformData) (interface{}, error) { + data := d.HydrateItem.(network.Probe) + loadBalancerName := strings.Split(*data.ID, "/")[8] + return loadBalancerName, nil +} diff --git a/docs/tables/azure_lb_probe.md b/docs/tables/azure_lb_probe.md new file mode 100644 index 00000000..f69b41e8 --- /dev/null +++ b/docs/tables/azure_lb_probe.md @@ -0,0 +1,47 @@ +# Table: azure_lb_probe + +When using load-balancing rules with Azure Load Balancer, you need to specify health probes to allow Load Balancer to detect the backend endpoint status. The configuration of the health probe and probe responses determine which backend pool instances will receive new flows. You can use health probes to detect the failure of an application on a backend endpoint. + +## Examples + +### Basic info + +```sql +select + id, + name, + type, + provisioning_state, + load_balancer_name, + port +from + azure_lb_probe; +``` + +### List failed load balancer probes + +```sql +select + id, + name, + type, + provisioning_state +from + azure_lb_probe +where + provisioning_state = 'Failed'; +``` + +### List load balancer probes order by interval + +```sql +select + id, + name, + type, + interval_in_seconds +from + azure_lb_probe +order by + interval_in_seconds; +``` \ No newline at end of file