diff --git a/azure-test/tests/azure_postgresql_flexible_server/dependencies.txt b/azure-test/tests/azure_postgresql_flexible_server/dependencies.txt new file mode 100644 index 00000000..e69de29b diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-get-expected.json b/azure-test/tests/azure_postgresql_flexible_server/test-get-expected.json new file mode 100644 index 00000000..7d8747e5 --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-get-expected.json @@ -0,0 +1,9 @@ +[ + { + "administrator_login": "psqladmin", + "name": "{{ resourceName }}", + "resource_group": "{{ resourceName }}", + "subscription_id": "{{ output.subscription_id.value }}", + "version": "12" + } +] diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-get-query.sql b/azure-test/tests/azure_postgresql_flexible_server/test-get-query.sql new file mode 100644 index 00000000..1b03450b --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-get-query.sql @@ -0,0 +1,3 @@ +select name, version, administrator_login, resource_group, subscription_id +from azure_postgresql_flexible_server +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-list-expected.json b/azure-test/tests/azure_postgresql_flexible_server/test-list-expected.json new file mode 100644 index 00000000..82128338 --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-list-expected.json @@ -0,0 +1,6 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}" + } +] diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-list-query.sql b/azure-test/tests/azure_postgresql_flexible_server/test-list-query.sql new file mode 100644 index 00000000..6b41986f --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-list-query.sql @@ -0,0 +1,3 @@ +select id, name +from azure_postgresql_flexible_server +where name = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-not-found-expected.json b/azure-test/tests/azure_postgresql_flexible_server/test-not-found-expected.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-not-found-expected.json @@ -0,0 +1 @@ +[] diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-not-found-query.sql b/azure-test/tests/azure_postgresql_flexible_server/test-not-found-query.sql new file mode 100644 index 00000000..806e0325 --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, akas, title +from azure_postgresql_flexible_server +where name = 'dummy-{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-turbot-expected.json b/azure-test/tests/azure_postgresql_flexible_server/test-turbot-expected.json new file mode 100644 index 00000000..682002ac --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/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 }}" + } +] diff --git a/azure-test/tests/azure_postgresql_flexible_server/test-turbot-query.sql b/azure-test/tests/azure_postgresql_flexible_server/test-turbot-query.sql new file mode 100644 index 00000000..9f37ad0f --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/test-turbot-query.sql @@ -0,0 +1,3 @@ +select name, akas, title, tags +from azure_postgresql_flexible_server +where name = '{{ resourceName }}' and resource_group = '{{ resourceName }}'; diff --git a/azure-test/tests/azure_postgresql_flexible_server/variables.json b/azure-test/tests/azure_postgresql_flexible_server/variables.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/variables.json @@ -0,0 +1 @@ +{} diff --git a/azure-test/tests/azure_postgresql_flexible_server/variables.tf b/azure-test/tests/azure_postgresql_flexible_server/variables.tf new file mode 100644 index 00000000..09b1ea51 --- /dev/null +++ b/azure-test/tests/azure_postgresql_flexible_server/variables.tf @@ -0,0 +1,115 @@ +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." +} + +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.3.0" + } + } +} + +# Configure the Microsoft Azure Provider +provider "azurerm" { + environment = var.azure_environment + subscription_id = var.azure_subscription + features {} +} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "East US" +} + +resource "azurerm_virtual_network" "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 + address_space = ["10.0.0.0/16"] +} + +resource "azurerm_subnet" "named_test_resource" { + name = var.resource_name + resource_group_name = azurerm_resource_group.named_test_resource.name + virtual_network_name = azurerm_virtual_network.named_test_resource.name + address_prefixes = ["10.0.2.0/24"] + service_endpoints = ["Microsoft.Storage"] + delegation { + name = "fs" + service_delegation { + name = "Microsoft.DBforPostgreSQL/flexibleServers" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action", + ] + } + } +} +resource "azurerm_private_dns_zone" "named_test_resource" { + name = "test.${var.resource_name}.postgres.database.azure.com" + resource_group_name = azurerm_resource_group.named_test_resource.name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "named_test_resource" { + name = "${var.resource_name}.com" + private_dns_zone_name = azurerm_private_dns_zone.named_test_resource.name + virtual_network_id = azurerm_virtual_network.named_test_resource.id + resource_group_name = azurerm_resource_group.named_test_resource.name + depends_on = [azurerm_subnet.named_test_resource] +} + +resource "azurerm_postgresql_flexible_server" "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 + version = "12" + delegated_subnet_id = azurerm_subnet.named_test_resource.id + private_dns_zone_id = azurerm_private_dns_zone.named_test_resource.id + administrator_login = "psqladmin" + administrator_password = "H@Sh1CoR3!" + zone = "1" + + storage_mb = 32768 + + sku_name = "GP_Standard_D4s_v3" + depends_on = [azurerm_private_dns_zone_virtual_network_link.named_test_resource] + tags = { + name = var.resource_name + } + +} + +output "resource_aka" { + value = "azure://${azurerm_postgresql_flexible_server.named_test_resource.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_postgresql_flexible_server.named_test_resource.id)}" +} + +output "resource_name" { + value = var.resource_name +} + +output "resource_id" { + value = azurerm_postgresql_flexible_server.named_test_resource.id +} + +output "subscription_id" { + value = var.azure_subscription +} \ No newline at end of file diff --git a/azure/table_azure_postgresql_flexible_server.go b/azure/table_azure_postgresql_flexible_server.go index 42561e2f..f1973eac 100644 --- a/azure/table_azure_postgresql_flexible_server.go +++ b/azure/table_azure_postgresql_flexible_server.go @@ -10,7 +10,7 @@ import ( "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" - "github.com/Azure/azure-sdk-for-go/profiles/latest/postgresql/mgmt/postgresqlflexibleservers" + armmypostgresflexibleservers "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v2" ) //// TABLE DEFINITION @@ -47,27 +47,141 @@ func tableAzurePostgreSqlFlexibleServer(_ context.Context) *plugin.Table { Description: "The type of the resource.", Type: proto.ColumnType_STRING, }, + { + Name: "state", + Description: "A state of a server that is visible to user.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.State"), + }, + { + Name: "availability_zone", + Description: "Availability zone information of the server.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.AvailabilityZone"), + }, + { + Name: "administrator_login", + Description: "The administrator's login name of a server. Can only be specified when the server is being created (and is required for creation).", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.AdministratorLogin"), + }, + { + Name: "create_mode", + Description: "The mode to create a new PostgreSQL server.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.CreateMode"), + }, + { + Name: "point_in_time_utc", + Description: "Restore point creation time (ISO8601 format), specifying the time to restore from. It's required when 'createMode' is 'PointInTimeRestore' or 'GeoRestore'.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromField("Properties.PointInTimeUTC").Transform(transform.NullIfZeroValue), + }, + { + Name: "replica_capacity", + Description: "Replicas allowed for a server.", + Type: proto.ColumnType_INT, + Transform: transform.FromField("Properties.ReplicaCapacity"), + }, + { + Name: "replication_role", + Description: "Replication role of the server.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.ReplicationRole"), + }, + { + Name: "source_server_resource_id", + Description: "The source server resource ID to restore from. It's required when 'createMode' is 'PointInTimeRestore' or 'GeoRestore' or 'Replica'.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.SourceServerResourceID"), + }, + { + Name: "version", + Description: "PostgreSQL Server version.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.Version"), + }, + { + Name: "fully_qualified_domain_name", + Description: "The fully qualified domain name of a server.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.FullyQualifiedDomainName"), + }, + { + Name: "minor_version", + Description: "The minor version of the server.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.MinorVersion"), + }, + { + Name: "public_network_access", + Description: "Public network access is enabled or not.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("Properties.Network.PublicNetworkAccess"), + }, { Name: "location", Description: "The geo-location where the resource lives.", Type: proto.ColumnType_STRING, }, - // We have raised a support request for this as SystemData is always null - // { - // Name: "system_data", - // Description: "The system data for the server.", - // Type: proto.ColumnType_JSON, - // }, + { + Name: "auth_config", + Description: "AuthConfig properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.AuthConfig"), + }, + { + Name: "backup", + Description: "Backup properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.Backup"), + }, + { + Name: "data_encryption", + Description: "Data encryption properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.DataEncryption"), + }, + { + Name: "high_availability", + Description: "High availability properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.HighAvailability"), + }, + { + Name: "maintenance_window", + Description: "Maintenance window properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.MaintenanceWindow"), + }, + { + Name: "network", + Description: "Network properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.Network"), + }, + { + Name: "storage", + Description: "Storage properties of a server.", + Type: proto.ColumnType_JSON, + Transform: transform.FromField("Properties.Storage"), + }, { Name: "sku", Description: "The SKU (pricing tier) of the server.", Type: proto.ColumnType_JSON, + Transform: transform.FromField("SKU"), + }, + { + Name: "system_data", + Description: "The system metadata relating to this server.", + Type: proto.ColumnType_JSON, }, { Name: "server_properties", Description: "Properties of the server.", Type: proto.ColumnType_JSON, - Transform: transform.FromField("ServerProperties").Transform(extractPostgresFlexibleServerProperties), + Transform: transform.FromField("Properties").Transform(extractPostgresFlexibleServerProperties), }, { Name: "flexible_server_configurations", @@ -76,6 +190,7 @@ func tableAzurePostgreSqlFlexibleServer(_ context.Context) *plugin.Table { Hydrate: listPostgreSQLFlexibleServersConfigurations, Transform: transform.FromValue(), }, + // Steampipe standard columns { Name: "title", @@ -115,38 +230,32 @@ func tableAzurePostgreSqlFlexibleServer(_ context.Context) *plugin.Table { //// LIST FUNCTION func listPostgreSqlFlexibleServers(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { - session, err := GetNewSession(ctx, d, "MANAGEMENT") + session, err := GetNewSessionUpdated(ctx, d) if err != nil { return nil, err } subscriptionID := session.SubscriptionID - client := postgresqlflexibleservers.NewServersClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) - client.Authorizer = session.Authorizer - resourceGroupName := h.Item.(resources.Group).Name - - result, err := client.ListByResourceGroup(ctx, *resourceGroupName) + client, err := armmypostgresflexibleservers.NewServersClient(subscriptionID, session.Cred, session.ClientOptions) if err != nil { - plugin.Logger(ctx).Error("listPostgreSqlFlexibleServers", "list", err) + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.listPostgreSqlFlexibleServers", "session_error", err) return nil, err } + resourceGroupName := h.Item.(resources.Group).Name - for _, server := range result.Values() { - d.StreamListItem(ctx, server) - // Check if context has been cancelled or if the limit has been hit (if specified) - // if there is a limit, it will return the number of rows required to reach this limit - if d.RowsRemaining(ctx) == 0 { - return nil, nil - } - } + input := &armmypostgresflexibleservers.ServersClientListByResourceGroupOptions{} + + pager := client.NewListByResourceGroupPager(*resourceGroupName, input) - for result.NotDone() { - err = result.NextWithContext(ctx) + for pager.More() { + page, err := pager.NextPage(ctx) if err != nil { + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.listPostgreSqlFlexibleServers", "api_error", err) return nil, err } - for _, server := range result.Values() { - d.StreamListItem(ctx, server) + for _, server := range page.Value { + d.StreamListItem(ctx, *server) + // Check if context has been cancelled or if the limit has been hit (if specified) // if there is a limit, it will return the number of rows required to reach this limit if d.RowsRemaining(ctx) == 0 { @@ -161,7 +270,6 @@ func listPostgreSqlFlexibleServers(ctx context.Context, d *plugin.QueryData, h * //// HYDRATE FUNCTIONS func getPostgreSqlFlexibleServer(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { - plugin.Logger(ctx).Trace("getPostgreSqlFlexibleServer") name := d.EqualsQualString("name") resourceGroup := d.EqualsQualString("resource_group") @@ -171,56 +279,60 @@ func getPostgreSqlFlexibleServer(ctx context.Context, d *plugin.QueryData, h *pl return nil, nil } - session, err := GetNewSession(ctx, d, "MANAGEMENT") + session, err := GetNewSessionUpdated(ctx, d) if err != nil { return nil, err } subscriptionID := session.SubscriptionID - client := postgresqlflexibleservers.NewServersClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) - client.Authorizer = session.Authorizer - - op, err := client.Get(ctx, resourceGroup, name) + client, err := armmypostgresflexibleservers.NewServersClient(subscriptionID, session.Cred, session.ClientOptions) if err != nil { - plugin.Logger(ctx).Error("getPostgreSqlFlexibleServer", "get", err) + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.getPostgreSqlFlexibleServer", "client_error", err) 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 + op, err := client.Get(ctx, resourceGroup, name, nil) + if err != nil { + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.getPostgreSqlFlexibleServer", "api_error", err) + return nil, err } - return nil, nil + return op.Server, nil } func listPostgreSQLFlexibleServersConfigurations(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { - plugin.Logger(ctx).Debug("listPostgreSQLFlexibleServersConfigurations") - server := h.Item.(postgresqlflexibleservers.Server) + server := h.Item.(armmypostgresflexibleservers.Server) resourceGroup := strings.Split(string(*server.ID), "/")[4] serverName := *server.Name - session, err := GetNewSession(ctx, d, "MANAGEMENT") + session, err := GetNewSessionUpdated(ctx, d) if err != nil { return nil, err } subscriptionID := session.SubscriptionID - client := postgresqlflexibleservers.NewConfigurationsClientWithBaseURI(session.ResourceManagerEndpoint, subscriptionID) - client.Authorizer = session.Authorizer - - op, err := client.ListByServer(ctx, resourceGroup, serverName) + client, err := armmypostgresflexibleservers.NewConfigurationsClient(subscriptionID, session.Cred, session.ClientOptions) if err != nil { - plugin.Logger(ctx).Error("listPostgreSQLFlexibleServersConfigurations", "list", err) + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.listPostgreSQLFlexibleServersConfigurations", "client_error", err) return nil, err } var postgreSQLFlexibleServersConfigurations []map[string]interface{} - for _, i := range op.Values() { - postgreSQLFlexibleServersConfigurations = append(postgreSQLFlexibleServersConfigurations, extractpostgreSQLFlexibleServersconfiguration(i)) + input := &armmypostgresflexibleservers.ConfigurationsClientListByServerOptions{} + pager := client.NewListByServerPager(resourceGroup, serverName, input) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + plugin.Logger(ctx).Error("azure_postgresql_flexible_server.listPostgreSQLFlexibleServersConfigurations", "api_error", err) + return nil, err + } + + for _, configuration := range page.Value { + postgreSQLFlexibleServersConfigurations = append(postgreSQLFlexibleServersConfigurations, extractpostgreSQLFlexibleServersconfiguration(*configuration)) + } } return postgreSQLFlexibleServersConfigurations, nil @@ -229,7 +341,7 @@ func listPostgreSQLFlexibleServersConfigurations(ctx context.Context, d *plugin. //// TRANSFORM FUNCTION // If we return the API response directly, the output will not provide the properties of Configurations -func extractpostgreSQLFlexibleServersconfiguration(i postgresqlflexibleservers.Configuration) map[string]interface{} { +func extractpostgreSQLFlexibleServersconfiguration(i armmypostgresflexibleservers.Configuration) map[string]interface{} { postgreSQLFlexibleServersconfiguration := make(map[string]interface{}) if i.ID != nil { @@ -241,17 +353,17 @@ func extractpostgreSQLFlexibleServersconfiguration(i postgresqlflexibleservers.C if i.Type != nil { postgreSQLFlexibleServersconfiguration["Type"] = *i.Type } - if i.ConfigurationProperties != nil { - postgreSQLFlexibleServersconfiguration["ConfigurationProperties"] = *i.ConfigurationProperties + if i.Properties != nil { + postgreSQLFlexibleServersconfiguration["ConfigurationProperties"] = *i.Properties } return postgreSQLFlexibleServersconfiguration } func extractPostgresFlexibleServerProperties(ctx context.Context, d *transform.TransformData) (interface{}, error) { - conf := d.HydrateItem.(postgresqlflexibleservers.Server) - if conf.ServerProperties != nil { - return structToMap(reflect.ValueOf(*conf.ServerProperties)), nil + conf := d.HydrateItem.(armmypostgresflexibleservers.Server) + if conf.Properties != nil { + return structToMap(reflect.ValueOf(*conf.Properties)), nil } return nil, nil } diff --git a/go.mod b/go.mod index 287696e1..e3e1947c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/guestconfiguration/armguestconfiguration v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managedservices/armmanagedservices v0.7.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v2 v2.1.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/recoveryservices/armrecoveryservicesbackup/v3 v3.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 github.com/Azure/azure-storage-blob-go v0.12.0 diff --git a/go.sum b/go.sum index a2f1039a..b8d5c4d6 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managedservices/armmanaged github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managedservices/armmanagedservices v0.7.0/go.mod h1:UmV8UnyCQ+05EvopWqF6CQsFWI5FWcp/AXGVkJguv9E= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0 h1:Ds0KRF8ggpEGg4Vo42oX1cIt/IfOhHWJBikksZbVxeg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor v0.11.0/go.mod h1:jj6P8ybImR+5topJ+eH6fgcemSFBmU6/6bFF8KkwuDI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v2 v2.1.1 h1:iu8l6XmviyuNbBQ/WO5shgLBU2+g18Cdw1iE1Xk2+ig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v2 v2.1.1/go.mod h1:8Anbzn23yMdpl2DDY4qnFEPY9Vf6CoLphIt4mX59McI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/recoveryservices/armrecoveryservicesbackup/v3 v3.0.0 h1:BIvscO5ZFKaEHoix9jAV6vbatKLiDNb5pj8XjzQR2g4= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/recoveryservices/armrecoveryservicesbackup/v3 v3.0.0/go.mod h1:4uHLkZ3JvDvacFcEyB0T/cCeVqlvtHHA7DjvpgvlpdA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=