diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/README.md b/tools/importer-rest-api-specs/components/parser/dataworkarounds/README.md new file mode 100644 index 00000000000..583fe4e500b --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/README.md @@ -0,0 +1,5 @@ +## Swagger Data Workarounds + +This package provides temporary Swagger Data Workarounds whilst the source data is being corrected within [the `Azure/azure-rest-api-specs` repository](https://github.com/Azure/azure-rest-api-specs). + +Whilst this approach isn't ideal, since each workaround must be accompanied by a Pull Request to fix the incorrect data upstream, we believe this is a pragmatic solution to both unblock us in the short-term and fix the source data to enable quality improvements in the medium/long-term. diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/interface.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/interface.go new file mode 100644 index 00000000000..728919006bd --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/interface.go @@ -0,0 +1,16 @@ +package dataworkarounds + +import ( + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" +) + +type workaround interface { + // IsApplicable determines whether this workaround is applicable for this AzureApiDefinition + IsApplicable(apiDefinition *models.AzureApiDefinition) bool + + // Name returns the Service Name and associated Pull Request number + Name() string + + // Process takes the apiDefinition and applies the Workaround to this AzureApiDefinition + Process(apiDefinition models.AzureApiDefinition) (*models.AzureApiDefinition, error) +} diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_containerservice_21394.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_containerservice_21394.go new file mode 100644 index 00000000000..68e40b1ebfb --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_containerservice_21394.go @@ -0,0 +1,42 @@ +package dataworkarounds + +import ( + "fmt" + + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" +) + +var _ workaround = workaroundContainerService21394{} + +// workaroundContainerService21394 works around the `DnsPrefix` field being required but being marked as Optional +// Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/21394 +type workaroundContainerService21394 struct{} + +func (workaroundContainerService21394) IsApplicable(apiDefinition *models.AzureApiDefinition) bool { + return apiDefinition.ServiceName == "ContainerService" && apiDefinition.ApiVersion == "2022-09-02-preview" +} + +func (workaroundContainerService21394) Name() string { + return "ContainerService / 21394" +} + +func (workaroundContainerService21394) Process(apiDefinition models.AzureApiDefinition) (*models.AzureApiDefinition, error) { + resource, ok := apiDefinition.Resources["Fleets"] + if !ok { + return nil, fmt.Errorf("couldn't find API Resource Fleets") + } + model, ok := resource.Models["FleetHubProfile"] + if !ok { + return nil, fmt.Errorf("couldn't find Model FleetHubProfile") + } + field, ok := model.Fields["DnsPrefix"] + if !ok { + return nil, fmt.Errorf("couldn't find field DnsPrefix within model FleetHubProfile") + } + field.Required = true + + model.Fields["DnsPrefix"] = field + resource.Models["FleetHubProfile"] = model + apiDefinition.Resources["Fleets"] = resource + return &apiDefinition, nil +} diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_loadtest_20961.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_loadtest_20961.go new file mode 100644 index 00000000000..98127234e98 --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_loadtest_20961.go @@ -0,0 +1,48 @@ +package dataworkarounds + +import ( + "fmt" + + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" +) + +var _ workaround = workaroundLoadTest20961{} + +// workaroundLoadTest20961 works around the Patch Model having no type for the Tags field (which is parsed +// as an Object instead). +// Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/20961 +type workaroundLoadTest20961 struct { +} + +func (workaroundLoadTest20961) IsApplicable(apiDefinition *models.AzureApiDefinition) bool { + serviceMatches := apiDefinition.ServiceName == "LoadTestService" + apiVersionMatches := apiDefinition.ApiVersion == "2021-12-01-preview" || apiDefinition.ApiVersion == "2022-04-15-preview" || apiDefinition.ApiVersion == "2022-12-01" + return serviceMatches && apiVersionMatches +} + +func (workaroundLoadTest20961) Name() string { + return "LoadTest / 20961" +} + +func (workaroundLoadTest20961) Process(apiDefinition models.AzureApiDefinition) (*models.AzureApiDefinition, error) { + resource, ok := apiDefinition.Resources["LoadTests"] + if !ok { + return nil, fmt.Errorf("couldn't find API Resource LoadTests") + } + model, ok := resource.Models["LoadTestResourcePatchRequestBody"] + if !ok { + return nil, fmt.Errorf("couldn't find Model LoadTestResourcePatchRequestBody") + } + field, ok := model.Fields["Tags"] + if !ok { + return nil, fmt.Errorf("couldn't find field Tags within model LoadTestResourcePatchRequestBody") + } + tagsType := models.CustomFieldTypeTags + field.CustomFieldType = &tagsType + field.ObjectDefinition = nil + + model.Fields["Tags"] = field + resource.Models["LoadTestResourcePatchRequestBody"] = model + apiDefinition.Resources["LoadTests"] = resource + return &apiDefinition, nil +} diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_media_21581.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_media_21581.go new file mode 100644 index 00000000000..dfcc5b0dbb9 --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_media_21581.go @@ -0,0 +1,34 @@ +package dataworkarounds + +import "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" + +var _ workaround = workaroundMedia21581{} + +// workaroundMedia21581 works around the Update operation having the incorrect Swagger Tag +// (StreamingEndpoint rather than StreamingEndpoints). +// Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/21581 +type workaroundMedia21581 struct { +} + +func (workaroundMedia21581) IsApplicable(apiDefinition *models.AzureApiDefinition) bool { + return apiDefinition.ServiceName == "Media" && apiDefinition.ApiVersion == "2020-05-01" +} + +func (workaroundMedia21581) Name() string { + return "Media / 21581" +} + +func (workaroundMedia21581) Process(apiDefinition models.AzureApiDefinition) (*models.AzureApiDefinition, error) { + singular, singularExists := apiDefinition.Resources["StreamingEndpoint"] + plural, pluralExists := apiDefinition.Resources["StreamingEndpoints"] + if singularExists && pluralExists { + updateOperation, ok := singular.Operations["Update"] + if ok { + // NOTE: we should be moving the Model too, but as it's the same as for Create this should be fine + plural.Operations["Update"] = updateOperation + apiDefinition.Resources["StreamingEndpoints"] = plural + delete(apiDefinition.Resources, "StreamingEndpoint") + } + } + return &apiDefinition, nil +} diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_recoveryservicessiterecovery_21667.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_recoveryservicessiterecovery_21667.go new file mode 100644 index 00000000000..6c1c181a29f --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workaround_recoveryservicessiterecovery_21667.go @@ -0,0 +1,191 @@ +package dataworkarounds + +import ( + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" + "github.com/hashicorp/pandora/tools/sdk/resourcemanager" +) + +var _ workaround = workaroundRecoveryServicesSiteRecovery21667{} + +// workaroundRecoveryServicesSiteRecovery21667 works around the Resource ID Segments being inconsistent within +// the RecoveryServicesSiteRecovery Resource Provider - namely that `replicatedProtectedItemName` should be +// `replicationProtectedItemName` - but is used interchangeably. +// +// Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/21667 +type workaroundRecoveryServicesSiteRecovery21667 struct { +} + +func (workaroundRecoveryServicesSiteRecovery21667) IsApplicable(apiDefinition *models.AzureApiDefinition) bool { + serviceMatches := apiDefinition.ServiceName == "RecoveryServicesSiteRecovery" + apiVersionMatches := apiDefinition.ApiVersion == "2022-05-01" || apiDefinition.ApiVersion == "2022-10-01" + return serviceMatches && apiVersionMatches +} + +func (workaroundRecoveryServicesSiteRecovery21667) Name() string { + return "RecoveryServicesSiteRecovery / 21667" +} + +func (w workaroundRecoveryServicesSiteRecovery21667) Process(apiDefinition models.AzureApiDefinition) (*models.AzureApiDefinition, error) { + resourcesToPatch := []string{ + "RecoveryPoints", + "ReplicationProtectedItems", + "TargetComputeSizes", + } + for _, resourceName := range resourcesToPatch { + if resource, ok := apiDefinition.Resources[resourceName]; ok { + if rid, ok := resource.ResourceIds["ReplicationProtectedItem"]; ok { + if rid.Matches(w.expectedResourceId()) { + rid.Segments = w.correctedResourceId().Segments + } + resource.ResourceIds["ReplicationProtectedItem"] = rid + } + apiDefinition.Resources[resourceName] = resource + } + } + return &apiDefinition, nil +} + +func (w workaroundRecoveryServicesSiteRecovery21667) expectedResourceId() models.ParsedResourceId { + return models.ParsedResourceId{ + Segments: []resourcemanager.ResourceIdSegment{ + { + Type: resourcemanager.StaticSegment, + Name: "staticSubscriptions", + FixedValue: pointer.To("subscriptions"), + }, + { + Type: resourcemanager.SubscriptionIdSegment, + Name: "subscriptionId", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticResourceGroups", + FixedValue: pointer.To("resourceGroups"), + }, + { + Type: resourcemanager.ResourceGroupSegment, + Name: "resourceGroupName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticProviders", + FixedValue: pointer.To("providers"), + }, + { + Type: resourcemanager.ResourceProviderSegment, + Name: "staticMicrosoftRecoveryServices", + FixedValue: pointer.To("Microsoft.RecoveryServices"), + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticVaults", + FixedValue: pointer.To("vaults"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "resourceName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationFabrics", + FixedValue: pointer.To("replicationFabrics"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "fabricName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationProtectionContainers", + FixedValue: pointer.To("replicationProtectionContainers"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "protectionContainerName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationProtectedItems", + FixedValue: pointer.To("replicationProtectedItems"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "replicatedProtectedItemName", + }, + }, + Constants: map[string]resourcemanager.ConstantDetails{}, + } +} + +func (w workaroundRecoveryServicesSiteRecovery21667) correctedResourceId() models.ParsedResourceId { + return models.ParsedResourceId{ + Segments: []resourcemanager.ResourceIdSegment{ + { + Type: resourcemanager.StaticSegment, + Name: "staticSubscriptions", + FixedValue: pointer.To("subscriptions"), + }, + { + Type: resourcemanager.SubscriptionIdSegment, + Name: "subscriptionId", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticResourceGroups", + FixedValue: pointer.To("resourceGroups"), + }, + { + Type: resourcemanager.ResourceGroupSegment, + Name: "resourceGroupName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticProviders", + FixedValue: pointer.To("providers"), + }, + { + Type: resourcemanager.ResourceProviderSegment, + Name: "staticMicrosoftRecoveryServices", + FixedValue: pointer.To("Microsoft.RecoveryServices"), + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticVaults", + FixedValue: pointer.To("vaults"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "resourceName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationFabrics", + FixedValue: pointer.To("replicationFabrics"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "fabricName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationProtectionContainers", + FixedValue: pointer.To("replicationProtectionContainers"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "protectionContainerName", + }, + { + Type: resourcemanager.StaticSegment, + Name: "staticReplicationProtectedItems", + FixedValue: pointer.To("replicationProtectedItems"), + }, + { + Type: resourcemanager.UserSpecifiedSegment, + Name: "replicationProtectedItemName", + }, + }, + Constants: map[string]resourcemanager.ConstantDetails{}, + } +} diff --git a/tools/importer-rest-api-specs/components/parser/dataworkarounds/workarounds.go b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workarounds.go new file mode 100644 index 00000000000..ac6abb81bae --- /dev/null +++ b/tools/importer-rest-api-specs/components/parser/dataworkarounds/workarounds.go @@ -0,0 +1,35 @@ +package dataworkarounds + +import ( + "fmt" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" +) + +var workarounds = []workaround{ + workaroundContainerService21394{}, + workaroundLoadTest20961{}, + workaroundMedia21581{}, +} + +func ApplyWorkarounds(input []models.AzureApiDefinition, logger hclog.Logger) (*[]models.AzureApiDefinition, error) { + output := make([]models.AzureApiDefinition, 0) + logger.Trace("Processing Swagger Data Workarounds..") + for _, item := range input { + for _, fix := range workarounds { + if fix.IsApplicable(&item) { + logger.Trace(fmt.Sprintf("Applying Swagger Data Workaround %q to Service %q / API Version %q", fix.Name(), item.ServiceName, item.ApiVersion)) + updated, err := fix.Process(item) + if err != nil { + return nil, fmt.Errorf("applying Swagger Data Workaround %q to Service %q / API Version %q: %+v", fix.Name(), item.ServiceName, item.ApiVersion, err) + } + + item = *updated + } + } + output = append(output, item) + } + + return &output, nil +} diff --git a/tools/importer-rest-api-specs/components/parser/load_and_parse.go b/tools/importer-rest-api-specs/components/parser/load_and_parse.go index a78d3ced726..f6386fccc62 100644 --- a/tools/importer-rest-api-specs/components/parser/load_and_parse.go +++ b/tools/importer-rest-api-specs/components/parser/load_and_parse.go @@ -4,11 +4,10 @@ import ( "fmt" "strings" - "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" - - "github.com/hashicorp/pandora/tools/importer-rest-api-specs/components/parser/resourceids" - "github.com/hashicorp/go-hclog" + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/components/parser/dataworkarounds" + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/components/parser/resourceids" + "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" ) func LoadAndParseFiles(directory string, fileNames []string, serviceName, apiVersion string, logger hclog.Logger) (*models.AzureApiDefinition, error) { @@ -84,7 +83,7 @@ func LoadAndParseFiles(directory string, fileNames []string, serviceName, apiVer } logger.Trace("Applying overrides to workaround invalid Swagger Definitions..") - output, err := patchSwaggerData(out, logger.Named("Swagger Data Override")) + output, err := dataworkarounds.ApplyWorkarounds(out, logger.Named("Swagger Data Override")) if err != nil { return nil, fmt.Errorf("applying Swagger overrides: %+v", err) } diff --git a/tools/importer-rest-api-specs/components/parser/swagger_data_workarounds.go b/tools/importer-rest-api-specs/components/parser/swagger_data_workarounds.go deleted file mode 100644 index a5c712ef3db..00000000000 --- a/tools/importer-rest-api-specs/components/parser/swagger_data_workarounds.go +++ /dev/null @@ -1,85 +0,0 @@ -package parser - -import ( - "fmt" - - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/pandora/tools/importer-rest-api-specs/models" -) - -func patchSwaggerData(input []models.AzureApiDefinition, logger hclog.Logger) (*[]models.AzureApiDefinition, error) { - output := make([]models.AzureApiDefinition, 0) - - // NOTE: each override in this file is temporary until the associated upstream Swagger PR is merged. - - for _, item := range input { - // This works around the Patch Model having no type for the Tags field (which is parsed as an Object instead) - // Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/20961 - if item.ServiceName == "LoadTestService" && (item.ApiVersion == "2021-12-01-preview" || item.ApiVersion == "2022-04-15-preview" || item.ApiVersion == "2022-12-01") { - logger.Trace(fmt.Sprintf("Processing Overrides for Service %q / API Version %q..", item.ServiceName, item.ApiVersion)) - resource, ok := item.Resources["LoadTests"] - if !ok { - return nil, fmt.Errorf("couldn't find API Resource LoadTests") - } - model, ok := resource.Models["LoadTestResourcePatchRequestBody"] - if !ok { - return nil, fmt.Errorf("couldn't find Model LoadTestResourcePatchRequestBody") - } - field, ok := model.Fields["Tags"] - if !ok { - return nil, fmt.Errorf("couldn't find field Tags within model LoadTestResourcePatchRequestBody") - } - tagsType := models.CustomFieldTypeTags - field.CustomFieldType = &tagsType - field.ObjectDefinition = nil - - model.Fields["Tags"] = field - resource.Models["LoadTestResourcePatchRequestBody"] = model - item.Resources["LoadTests"] = resource - } - - // Works around the `DnsPrefix` field being required but being marked as Optional - // Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/21394 - if item.ServiceName == "ContainerService" && item.ApiVersion == "2022-09-02-preview" { - logger.Trace(fmt.Sprintf("Processing Overrides for Service %q / API Version %q..", item.ServiceName, item.ApiVersion)) - resource, ok := item.Resources["Fleets"] - if !ok { - return nil, fmt.Errorf("couldn't find API Resource Fleets") - } - model, ok := resource.Models["FleetHubProfile"] - if !ok { - return nil, fmt.Errorf("couldn't find Model FleetHubProfile") - } - field, ok := model.Fields["DnsPrefix"] - if !ok { - return nil, fmt.Errorf("couldn't find field DnsPrefix within model FleetHubProfile") - } - field.Required = true - - model.Fields["DnsPrefix"] = field - resource.Models["FleetHubProfile"] = model - item.Resources["Fleets"] = resource - } - - // Works around the Update operation having the incorrect Swagger Tag (StreamingEndpoint - // rather than StreamingEndpoints). - // Swagger PR: https://github.com/Azure/azure-rest-api-specs/pull/21581 - if item.ServiceName == "Media" && item.ApiVersion == "2020-05-01" { - singular, singularExists := item.Resources["StreamingEndpoint"] - plural, pluralExists := item.Resources["StreamingEndpoints"] - if singularExists && pluralExists { - updateOperation, ok := singular.Operations["Update"] - if ok { - // NOTE: we should be moving the Model too, but as it's the same as for Create this should be fine - plural.Operations["Update"] = updateOperation - item.Resources["StreamingEndpoints"] = plural - delete(item.Resources, "StreamingEndpoint") - } - } - } - - output = append(output, item) - } - - return &output, nil -}