diff --git a/provider/pkg/gen/schema.go b/provider/pkg/gen/schema.go index 62c93b0d3089..b558d24cddaa 100644 --- a/provider/pkg/gen/schema.go +++ b/provider/pkg/gen/schema.go @@ -822,80 +822,6 @@ func (g *packageGenerator) findResourceVariants(resource *openapi.ResourceSpec) return result, nil } -// dedupResourceNameByPath returns a modified resource name (`typeName`) if the resource is mapped to multiple API -// paths. For instance, the deprecated "single server" resources in `dbformysql` and `dbforpostgresql` are renamed -// to `SingleServerResource`. -// TODO,tkappler check each one if we can just get rid of an old API version instead of doing this. -func dedupResourceNameByPath(moduleName openapi.ModuleName, typeName, canonPath string) string { - result := typeName - - prefix := func(prefix string) string { - if !strings.HasPrefix(typeName, prefix) { - return prefix + typeName - } - return typeName - } - - switch moduleName.Lowered() { - case "cache": - if strings.Contains(canonPath, "/redis/") { - result = prefix("Redis") - } else if strings.Contains(canonPath, "/redisenterprise/") { - result = prefix("RedisEnterprise") - } - // $ rg --only-matching --no-filename --glob '!examples' 'providers/Microsoft.DBforMySQL/.+?/' azure-rest-api-specs/specification/ | sort | uniq - // providers/Microsoft.DBforMySQL/flexibleServers/ - // providers/Microsoft.DBforMySQL/servers/ - case "dbformysql": - if strings.Contains(canonPath, "/servers/") { - result = prefix("SingleServer") - } - // $ rg --only-matching --no-filename --glob '!examples' 'providers/Microsoft.DBforPostgreSQL/.+?/' azure-rest-api-specs/specification/ | sort | uniq - // providers/Microsoft.DBforPostgreSQL/flexibleServers/ - // providers/Microsoft.DBforPostgreSQL/serverGroupsv2/ - // providers/Microsoft.DBforPostgreSQL/servers/ - case "dbforpostgresql": - if strings.Contains(canonPath, "/servers/") { - result = prefix("SingleServer") - } else if strings.Contains(canonPath, "/servergroupsv2/") { - result = prefix("ServerGroup") - } - case "documentdb": - if strings.Contains(canonPath, "/mongoclusters/") { - prefix("MongoCluster") - } - case "hdinsight": - if strings.Contains(canonPath, "/clusterpools/") { - result = prefix("ClusterPool") - } - case "hybridcontainerservice": - if strings.Contains(canonPath, "/provisionedclusterinstances/") { - result = prefix("ClusterInstance") - } - case "labservices": - // /labaccounts is an old API that only occurs in 2018 but we support it in v2 - if strings.Contains(canonPath, "/labaccounts/") { - result = prefix("LabAccount") - } - case "migrate": - if strings.Contains(canonPath, "/assessmentprojects/") { - result = prefix("AssessmentProjects") - } - case "mobilenetwork": - if strings.Contains(canonPath, "/simgroups/") { - result = prefix("SimGroup") - } - case "netapp": - if strings.Contains(canonPath, "/backupvaults/") { - result = prefix("BackupVault") - } else if strings.Contains(canonPath, "/capacitypools/") { - result = prefix("CapacityPool") - } - } - - return result -} - // recordPath adds path to keep track of all API paths a resource is mapped to. func (g *packageGenerator) recordPath(typeName openapi.ResourceName, canonPath string, apiVersion openapi.ApiVersion) { // Some resources have a /default path, e.g., azure-native:azurestackhci:GuestAgent has conflicting paths @@ -935,9 +861,6 @@ func (g *packageGenerator) genResourceVariant(apiSpec *openapi.ResourceSpec, res canonPath := paths.NormalizePath(resource.Path) typeName := resource.typeName - if g.majorVersion > 3 { - typeName = dedupResourceNameByPath(g.moduleName, resource.typeName, canonPath) - } resourceTok := generateTok(g.moduleName, typeName, g.sdkVersion) if !g.versioning.ShouldInclude(g.moduleName, g.apiVersion, typeName, resourceTok) { diff --git a/provider/pkg/gen/schema_test.go b/provider/pkg/gen/schema_test.go index a83cfd3b7d73..439688ec6b44 100644 --- a/provider/pkg/gen/schema_test.go +++ b/provider/pkg/gen/schema_test.go @@ -361,28 +361,6 @@ func TestGoModuleName(t *testing.T) { }) } -func TestDedupResourceNameByPath(t *testing.T) { - t.Run("no change", func(t *testing.T) { - assert.Equal(t, "Resource", dedupResourceNameByPath("compute", "Resource", "/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Compute/virtualmachines/{}")) - }) - - t.Run("dbformysql single server", func(t *testing.T) { - assert.Equal(t, "SingleServerResource", dedupResourceNameByPath("dbformysql", "Resource", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.DBforMySQL/servers/{}")) - }) - - t.Run("dbformysql flexible server", func(t *testing.T) { - assert.Equal(t, "Resource", dedupResourceNameByPath("dbformysql", "Resource", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.DBforMySQL/flexibleservers/{}")) - }) - - t.Run("dbforpostgresql single server", func(t *testing.T) { - assert.Equal(t, "SingleServerResource", dedupResourceNameByPath("dbforpostgresql", "Resource", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.DBforPostgreSQL/servers/{}")) - }) - - t.Run("dbforpostgresql flexible server", func(t *testing.T) { - assert.Equal(t, "Resource", dedupResourceNameByPath("dbforpostgresql", "Resource", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.DBforPostgreSQL/flexibleservers/{}")) - }) -} - func TestResourcePathTracker(t *testing.T) { t.Run("no conflicts, one module", func(t *testing.T) { tracker := newResourcesPathConflictsTracker() diff --git a/provider/pkg/gen/types.go b/provider/pkg/gen/types.go index 7cdd6dff69ca..13ec0cc4a146 100644 --- a/provider/pkg/gen/types.go +++ b/provider/pkg/gen/types.go @@ -24,6 +24,7 @@ import ( "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/convert" "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/openapi" "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/resources" + "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/version" "github.com/pulumi/pulumi/pkg/v3/codegen" pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" ) @@ -450,9 +451,17 @@ var typeNameOverrides = map[string]string{ // This one is not a disambiguation but a fix for a type name "String" that is not descriptive and leads to // generating invalid Java. "DatabaseWatcher.Target.String": "TargetCollectionStatus", + // SingleServer is different from just "Server", see the exception in resources.go ResourceName(). + "DBforPostgreSQL.SingleServer.Sku": "SingleServerSku", // Devices RP comes from "deviceprovisioningservices" and "iothub" which are similar but slightly different. // In particular, the IP Filter Rule has more properties in the DPS version. "Devices.IotDpsResource.IpFilterRule": "TargetIpFilterRule", + // See the exception for Microsoft.HDInsight in resources.go ResourceName(). + "HDInsight.ClusterPoolCluster.Sku": "ClusterPoolSku", + "HDInsight.ClusterPoolCluster.ComputeProfile": "ClusterPoolComputeProfile", + "HDInsight.ClusterPoolCluster.SshProfile": "ClusterPoolSshProfile", + + "HybridContainerService.ClusterInstanceAgentPool.Status": "AgentPoolProvisioningStatus", // Workbook vs. MyWorkbook types are slightly different. Probably, a bug in the spec, but we have to disambiguate. "Insights.MyWorkbook.ManagedIdentity": "MyManagedIdentity", "Insights.MyWorkbook.UserAssignedIdentities": "MyUserAssignedIdentities", @@ -480,11 +489,22 @@ var typeNameOverrides = map[string]string{ "SecurityInsights.WatchlistItem.UserInfo": "WatchlistUserInfo", } +var typeNameOverridesV3 = map[string]string{ + // DocumentDB.MongoCluster from /mongocluster uses a different private endpoint connection. + // The MongoCluster resource has the same name in v2 and v3, but the private endpoint connection was disambiguated in v3. + "DocumentDB.MongoCluster.PrivateEndpointConnection": "MongoClusterPrivateEndpointConnection", +} + func (m *moduleGenerator) typeNameOverride(typeName string) string { key := fmt.Sprintf("%s.%s.%s", m.moduleName, m.resourceName, typeName) if v, ok := typeNameOverrides[key]; ok { return v } + if version.GetVersion().Major >= 3 { + if v, ok := typeNameOverridesV3[key]; ok { + return v + } + } return typeName } diff --git a/provider/pkg/resources/resources.go b/provider/pkg/resources/resources.go index 950938018915..632e7e15c5c2 100644 --- a/provider/pkg/resources/resources.go +++ b/provider/pkg/resources/resources.go @@ -7,8 +7,10 @@ import ( "regexp" "sort" "strings" + "unicode" "github.com/gedex/inflector" + "github.com/pulumi/pulumi-azure-native/v2/provider/pkg/version" ) // SingleValueProperty is the name of the property that we insert into the schema for non-object type responses of invokes. @@ -463,6 +465,17 @@ type NameDisambiguation struct { // ResourceName constructs a name of a resource based on Get or List operation ID, // e.g. "Managers_GetActivationKey" -> "ManagerActivationKey". func ResourceName(operationID, path string) (string, *NameDisambiguation) { + return createResourceName(operationID, path, version.GetVersion().Major) +} + +func createResourceName(operationID, path string, majorVersion uint64) (string, *NameDisambiguation) { + if majorVersion >= 3 { + // Uppercase the first character of operationID + r := []rune(operationID) + r[0] = unicode.ToUpper(r[0]) + operationID = string(r) + } + normalizedID := strings.ReplaceAll(operationID, "-", "_") parts := strings.Split(normalizedID, "_") var name, verb string @@ -493,66 +506,61 @@ func ResourceName(operationID, path string) (string, *NameDisambiguation) { resourceName := name + subName - var nameDisambiuation *NameDisambiguation + return handleResourceNameSpecialCases(resourceName, operationID, path, majorVersion) +} - // Special cases +// handleResourceNameSpecialCases returns a modified resource name if the resource is mapped to multiple API paths. For +// instance, the deprecated "single server" resources in `dbformysql` and `dbforpostgresql` are renamed to `SingleServer`. +func handleResourceNameSpecialCases(resourceName, operationID, path string, majorVersion uint64) (string, *NameDisambiguation) { + newName := func(prefix, newName string) (string, *NameDisambiguation) { + // Don't generate "RedisRedis" + if prefix != "" && !strings.HasPrefix(resourceName, prefix) { + newName = prefix + newName + } - // Manual override to resolve ambiguity between public and private RecordSet. - // See https://github.com/pulumi/pulumi-azure-native/issues/583. - // To be removed with https://github.com/pulumi/pulumi-azure-native/issues/690. - if resourceName == "RecordSet" && strings.Contains(path, "/providers/Microsoft.Network/privateDnsZones/") { - newName := "PrivateRecordSet" - nameDisambiuation = &NameDisambiguation{ - OperationID: operationID, - Path: path, - GeneratedName: resourceName, - DisambiguatedName: newName, + var nameDisambiguation *NameDisambiguation + if newName != resourceName { + nameDisambiguation = &NameDisambiguation{ + OperationID: operationID, + Path: path, + GeneratedName: resourceName, + DisambiguatedName: newName, + } } - resourceName = newName + + return newName, nameDisambiguation } - // Cognitive Services has global and per-account commitment plans with the same name. + var nameDisambiguation *NameDisambiguation + lowerPath := strings.ToLower(path) + + // Microsoft.CognitiveServices has global and per-account commitment plans with the same name. // The global ones are new, introduced in 2022-12-01, so we rename them. - // TODO,tkappler The global plan still has the description "Cognitive Services account - // commitment plan." - upstream issue? + // The global plan still has the description "Cognitive Services account commitment plan." - upstream issue? if resourceName == "CommitmentPlan" && strings.Contains(path, "/providers/Microsoft.CognitiveServices/commitmentPlans/") { - newName := "SharedCommitmentPlan" - nameDisambiuation = &NameDisambiguation{ - OperationID: operationID, - Path: path, - GeneratedName: resourceName, - DisambiguatedName: newName, - } - resourceName = newName - } - - // Redis and RedisEnterprise are essentially distinct resources sharing the Microsoft.Cache - // namespace. It works out ok because each API version has only one of them, and of the shared - // types only PrivateEndpointConnection clashes. - if resourceName == "PrivateEndpointConnection" && strings.Contains(path, "/providers/Microsoft.Cache/redisEnterprise/") { - newName := "EnterprisePrivateEndpointConnection" - nameDisambiuation = &NameDisambiguation{ - OperationID: operationID, - Path: path, - GeneratedName: resourceName, - DisambiguatedName: newName, - } - resourceName = newName + resourceName, nameDisambiguation = newName("", "SharedCommitmentPlan") + } + + // Microsoft.NetApp + if strings.Contains(lowerPath, "/providers/microsoft.netapp/backupvaults/") { + resourceName, nameDisambiguation = newName("BackupVault", resourceName) } + // Microsoft.Network + // Manual override to resolve ambiguity between public and private RecordSet. + // See https://github.com/pulumi/pulumi-azure-native/issues/583. + // To be removed with https://github.com/pulumi/pulumi-azure-native/issues/690. + if resourceName == "RecordSet" && strings.Contains(path, "/providers/Microsoft.Network/privateDnsZones/") { + resourceName, nameDisambiguation = newName("", "PrivateRecordSet") + } + + // Microsoft.Network // Both are virtual network links, but the other side of the link is a different resource and // the links have different properties. // https://learn.microsoft.com/en-us/azure/dns/dns-private-resolver-overview#virtual-network-links // https://learn.microsoft.com/en-us/azure/dns/private-dns-virtual-network-links if resourceName == "VirtualNetworkLink" && strings.Contains(path, "/providers/Microsoft.Network/dnsForwardingRulesets/") { - newName := "PrivateResolverVirtualNetworkLink" - nameDisambiuation = &NameDisambiguation{ - OperationID: operationID, - Path: path, - GeneratedName: resourceName, - DisambiguatedName: newName, - } - resourceName = newName + resourceName, nameDisambiguation = newName("", "PrivateResolverVirtualNetworkLink") } // ServiceFabric introduced managed clusters in a new folder 'servicefabricmanagedclusters' but @@ -562,17 +570,84 @@ func ResourceName(operationID, path string) (string, *NameDisambiguation) { resourceName == "ApplicationType" || resourceName == "ApplicationTypeVersion" || resourceName == "Service") { - newName := "ManagedCluster" + resourceName - nameDisambiuation = &NameDisambiguation{ - OperationID: operationID, - Path: path, - GeneratedName: resourceName, - DisambiguatedName: newName, + resourceName, nameDisambiguation = newName("ManagedCluster", resourceName) + } + + if majorVersion < 3 { + if resourceName == "PrivateEndpointConnection" && strings.Contains(path, "/providers/Microsoft.Cache/redisEnterprise/") { + resourceName, nameDisambiguation = newName("", "EnterprisePrivateEndpointConnection") + } + } + + if majorVersion >= 3 { + // Microsoft.Cache + if strings.Contains(lowerPath, "/providers/microsoft.cache/redis/") { + // resourceName, nameDisambiguation = newName("Redis", resourceName) + } else if strings.Contains(lowerPath, "/providers/microsoft.cache/redisenterprise/") { + if resourceName != "RedisEnterprise" { + resourceName, nameDisambiguation = newName("Enterprise", resourceName) + } + } + + // Microsoft.DBforMySQL + if strings.Contains(lowerPath, "/providers/microsoft.dbformysql/servers/") { + if resourceName == "Server" { + resourceName, nameDisambiguation = newName("", "SingleServer") + } else { + resourceName, nameDisambiguation = newName("SingleServer", resourceName) + } + } + + // Microsoft.DBforPostgreSQL + if strings.Contains(lowerPath, "/providers/microsoft.dbforpostgresql/servers/") { + if resourceName == "Server" { + resourceName, nameDisambiguation = newName("", "SingleServer") + } else { + resourceName, nameDisambiguation = newName("SingleServer", resourceName) + } + } else if strings.Contains(lowerPath, "/providers/microsoft.dbforpostgresql/servergroupsv2/") { + resourceName, nameDisambiguation = newName("ServerGroup", resourceName) + } + + // Microsoft.DocumentDB + if strings.Contains(lowerPath, "/providers/microsoft.documentdb/mongoclusters/") { + resourceName, nameDisambiguation = newName("MongoCluster", resourceName) + } + + // Microsoft.HDInsight + if strings.Contains(lowerPath, "/providers/microsoft.hdinsight/clusterpools/") { + resourceName, nameDisambiguation = newName("ClusterPool", resourceName) + } + + // Microsoft.HybridContainerService + if strings.Contains(lowerPath, "/providers/microsoft.hybridcontainerservice/provisionedclusterinstances/") { + if !strings.Contains(resourceName, "ClusterInstance") { + resourceName, nameDisambiguation = newName("ClusterInstance", resourceName) + } + } + + // Microsoft.LabServices + if strings.Contains(lowerPath, "/providers/microsoft.labservices/labaccounts/") { + resourceName, nameDisambiguation = newName("LabAccount", resourceName) + } + + // Microsoft.Migrate + if strings.Contains(lowerPath, "/providers/microsoft.migrate/assessmentprojects/") { + resourceName, nameDisambiguation = newName("AssessmentProjects", resourceName) + } + + // Microsoft.MobileNetwork + if strings.Contains(lowerPath, "/providers/microsoft.mobilenetwork/simgroups/") { + resourceName, nameDisambiguation = newName("SimGroup", resourceName) + } + + // Microsoft.NetApp + if strings.Contains(lowerPath, "/providers/microsoft.netapp/netappaccounts/") && strings.Contains(lowerPath, "/capacitypools/") { + resourceName, nameDisambiguation = newName("CapacityPool", resourceName) } - resourceName = newName } - return resourceName, nameDisambiuation + return resourceName, nameDisambiguation } var referenceNameReplacer = strings.NewReplacer("CreateOrUpdateParameters", "", "Create", "", "Request", "") diff --git a/provider/pkg/resources/resources_test.go b/provider/pkg/resources/resources_test.go index 19869c8ea0aa..18d9566cb493 100644 --- a/provider/pkg/resources/resources_test.go +++ b/provider/pkg/resources/resources_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type resourceNameTestCase struct { @@ -14,9 +15,9 @@ type resourceNameTestCase struct { expected string } -func TestResourceName(t *testing.T) { +func TestStandardResourceNames(t *testing.T) { // Empty path means "doesn't matter", not that these resources don't have a path. - testCases2 := []resourceNameTestCase{ + testCases := []resourceNameTestCase{ {"GetUserSettings", "", "UserSettings"}, {"Mediaservices_Get", "", "MediaService"}, {"Redis_Get", "", "Redis"}, @@ -33,6 +34,24 @@ func TestResourceName(t *testing.T) { {"WebApps_ListApplicationSettings", "", "WebAppApplicationSettings"}, {"Products_GetProducts", "", "Products"}, {"PowerBIResources_ListByResourceName", "", "PowerBIResource"}, + } + + for _, tc := range testCases { + require.NotEmpty(t, tc.expected) // test invariant + for _, majorVersion := range []uint64{2, 3} { + actual, _ := createResourceName(tc.operationID, tc.path, majorVersion) + assert.Equal(t, tc.expected, actual) + } + } +} + +func TestSpecialResourceNames(t *testing.T) { + testCases := []resourceNameTestCase{ + { + "Servers_Get", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DBforPostgreSQL/flexibleServers/{serverName}", + "Server", + }, // An exception for https://github.com/pulumi/pulumi-azure-native/issues/583, disambiguated by path. { @@ -47,10 +66,39 @@ func TestResourceName(t *testing.T) { }, } - for _, tc := range testCases2 { - actual, _ := ResourceName(tc.operationID, tc.path) + for _, tc := range testCases { + require.NotEmpty(t, tc.expected) // test invariant + for _, majorVersion := range []uint64{2, 3} { + actual, _ := createResourceName(tc.operationID, tc.path, majorVersion) + assert.Equal(t, tc.expected, actual) + } + } +} + +func TestSpecialResourceNamesV3(t *testing.T) { + testCases := []resourceNameTestCase{ + { + "Servers_Get", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DBforPostgreSQL/servers/{serverName}", + "SingleServer", + }, + { + "Servers_Get", + "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.DBforMySQL/servers/{serverName}", + "SingleServer", + }, + } + + for _, tc := range testCases { + require.NotEmpty(t, tc.expected) // test invariant + actual, _ := createResourceName(tc.operationID, tc.path, 3) assert.Equal(t, tc.expected, actual) } + + for _, tc := range testCases { + actual, _ := createResourceName(tc.operationID, tc.path, 2) + assert.NotEqual(t, tc.expected, actual) + } } func TestAutoName(t *testing.T) { diff --git a/provider/pkg/versioning/gen.go b/provider/pkg/versioning/gen.go index eda6a4e7f908..7bb4f361a34b 100644 --- a/provider/pkg/versioning/gen.go +++ b/provider/pkg/versioning/gen.go @@ -46,6 +46,11 @@ func (v VersionMetadata) ShouldInclude(moduleName openapi.ModuleName, version *o } // Keep any resources in the previous version lock for easier migration if v.MajorVersion >= 3 && v.PreviousDefaultVersions.IsAtVersion(moduleName, typeName, *version) { + // We're making an exception for these types because we want to remove their previous default version since it + // was broken and had no Pulumi Cloud users according to Metabase. #3817 + if moduleName == "MachineLearningServices" && (typeName == "ConnectionRaiBlocklist" || typeName == "ConnectionRaiBlocklistItem") { + return false + } return true } // Exclude versions from removed versions diff --git a/versions/v3-config.yaml b/versions/v3-config.yaml index ffe02713c75a..f47b7d5860b0 100644 --- a/versions/v3-config.yaml +++ b/versions/v3-config.yaml @@ -379,7 +379,9 @@ HybridContainerService: exclusions: # Between type conflicts and partial version releases, we're fairly stuck on the version combinations that work. # Excluding some new resources from the default version and pinning to and older version as a workaround. + ClusterInstanceAgentPool: "2024-01-01" KubernetesVersions: "2024-01-01" + ProvisionedClusterInstance: "2024-01-01" VMSkus: "2024-01-01" listprovisionedClusterInstanceAdminKubeconfig: "2024-01-01" listprovisionedClusterInstanceUserKubeconfig: "2024-01-01" diff --git a/versions/v3-removed.json b/versions/v3-removed.json index 44d39300a1a0..cbdccbb8a6ac 100644 --- a/versions/v3-removed.json +++ b/versions/v3-removed.json @@ -62,7 +62,9 @@ "2016-01-01" ], "AzureStackHCI": [ - "2020-03-01-preview" + "2020-03-01-preview", + "2021-07-01-preview", + "2021-09-01-preview" ], "BareMetalInfrastructure": [], "Batch": [ @@ -284,7 +286,22 @@ "2020-03-01", "2020-04-01", "2020-06-01-preview", - "2020-09-01" + "2020-09-01", + "2021-01-15", + "2021-03-15", + "2021-04-01-preview", + "2021-04-15", + "2021-05-15", + "2021-06-15", + "2021-10-15", + "2021-10-15-preview", + "2022-02-15-preview", + "2022-05-15-preview", + "2022-05-15", + "2022-08-15-preview", + "2022-08-15", + "2022-11-15-preview", + "2022-11-15" ], "DomainRegistration": [ "2015-04-01", @@ -402,7 +419,9 @@ "2019-06-01", "2019-11-01", "2020-01-01", - "2020-02-18-preview" + "2020-02-18-preview", + "2020-03-01", + "2024-04-01-preview" ], "Maintenance": [ "2018-06-01-preview"