diff --git a/azurerm/internal/services/desktopvirtualization/resourceids.go b/azurerm/internal/services/desktopvirtualization/resourcesid.go similarity index 100% rename from azurerm/internal/services/desktopvirtualization/resourceids.go rename to azurerm/internal/services/desktopvirtualization/resourcesid.go diff --git a/azurerm/internal/services/media/client/client.go b/azurerm/internal/services/media/client/client.go index 4f97fa557586..4e5deb1b3956 100644 --- a/azurerm/internal/services/media/client/client.go +++ b/azurerm/internal/services/media/client/client.go @@ -6,8 +6,9 @@ import ( ) type Client struct { - ServicesClient *media.MediaservicesClient - AssetsClient *media.AssetsClient + ServicesClient *media.MediaservicesClient + AssetsClient *media.AssetsClient + TransformsClient *media.TransformsClient } func NewClient(o *common.ClientOptions) *Client { @@ -17,8 +18,12 @@ func NewClient(o *common.ClientOptions) *Client { AssetsClient := media.NewAssetsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&AssetsClient.Client, o.ResourceManagerAuthorizer) + TransformsClient := media.NewTransformsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&TransformsClient.Client, o.ResourceManagerAuthorizer) + return &Client{ - ServicesClient: &ServicesClient, - AssetsClient: &AssetsClient, + ServicesClient: &ServicesClient, + AssetsClient: &AssetsClient, + TransformsClient: &TransformsClient, } } diff --git a/azurerm/internal/services/media/media_services_account_resource.go b/azurerm/internal/services/media/media_services_account_resource.go index 868889a19359..8c8a59aa45c7 100644 --- a/azurerm/internal/services/media/media_services_account_resource.go +++ b/azurerm/internal/services/media/media_services_account_resource.go @@ -147,7 +147,7 @@ func resourceMediaServicesAccountCreateUpdate(d *schema.ResourceData, meta inter parameters.Identity = expandAzureRmMediaServiceIdentity(d) } - if v, ok := d.GetOk("storage_authentication"); ok { + if v, ok := d.GetOk("storage_authentication_type"); ok { parameters.StorageAuthentication = media.StorageAuthentication(v.(string)) } diff --git a/azurerm/internal/services/media/media_transform_resource.go b/azurerm/internal/services/media/media_transform_resource.go new file mode 100644 index 000000000000..2402af9dc291 --- /dev/null +++ b/azurerm/internal/services/media/media_transform_resource.go @@ -0,0 +1,459 @@ +package media + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/Azure/azure-sdk-for-go/services/mediaservices/mgmt/2020-05-01/media" + "github.com/hashicorp/go-azure-helpers/response" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/media/parse" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceMediaTransform() *schema.Resource { + return &schema.Resource{ + Create: resourceMediaTransformCreateUpdate, + Read: resourceMediaTransformRead, + Update: resourceMediaTransformCreateUpdate, + Delete: resourceMediaTransformDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.TransformID(id) + return err + }), + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile("^[-a-zA-Z0-9(_)]{1,128}$"), + "Transform name must be 1 - 128 characters long, can contain letters, numbers, underscores, and hyphens (but the first and last character must be a letter or number).", + ), + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "media_services_account_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile("^[-a-z0-9]{3,24}$"), + "Media Services Account name must be 3 - 24 characters long, contain only lowercase letters and numbers.", + ), + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.NoEmptyStrings, + }, + + "output": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "on_error_action": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.ContinueJob), string(media.StopProcessingJob), + }, true), + }, + "builtin_preset": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "preset_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.AACGoodQualityAudio), string(media.AdaptiveStreaming), + string(media.ContentAwareEncoding), string(media.ContentAwareEncodingExperimental), + string(media.CopyAllBitrateNonInterleaved), string(media.H264MultipleBitrate1080p), + string(media.H264MultipleBitrate720p), string(media.H264MultipleBitrateSD), + string(media.H264SingleBitrate1080p), string(media.H264SingleBitrate720p), + string(media.H264MultipleBitrateSD), + }, true), + }, + }, + }, + }, + "audio_analyzer_preset": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "audio_language": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "ar-EG", "ar-SY", "de-DE", "en-AU", "en-GB", "en-US", "es-ES", "es-MX", + "fr-FR", "hi-IN", "it-IT", "ja-JP", "ko-KR", "pt-BR", "ru-RU", "zh-CN", + }, true), + }, + "audio_analysis_mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.Basic), string(media.Standard), + }, true), + }, + }, + }, + }, + "video_analyzer_preset": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "audio_language": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "ar-EG", "ar-SY", "de-DE", "en-AU", "en-GB", "en-US", "es-ES", "es-MX", + "fr-FR", "hi-IN", "it-IT", "ja-JP", "ko-KR", "pt-BR", "ru-RU", "zh-CN", + }, true), + }, + "audio_analysis_mode": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.Basic), string(media.Standard), + }, true), + }, + "insights_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.AllInsights), string(media.AudioInsightsOnly), string(media.VideoInsightsOnly), + }, true), + }, + }, + }, + }, + "face_detector_preset": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "analysis_resolution": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.SourceResolution), string(media.StandardDefinition), + }, true), + }, + }, + }, + }, + "relative_priority": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(media.High), string(media.Normal), string(media.Low), + }, true), + }, + }, + }, + }, + }, + } +} + +func resourceMediaTransformCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Media.TransformsClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + transformName := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + accountName := d.Get("media_services_account_name").(string) + description := d.Get("description").(string) + + parameters := media.Transform{ + TransformProperties: &media.TransformProperties{ + Description: utils.String(description), + }, + } + + if v, ok := d.GetOk("output"); ok { + transformOutput, err := expandTransformOuputs(v.([]interface{})) + if err != nil { + return err + } + parameters.Outputs = transformOutput + } + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, accountName, transformName, parameters); err != nil { + return fmt.Errorf("Error creating Transform %q in Media Services Account %q (Resource Group %q): %+v", transformName, accountName, resourceGroup, err) + } + + transform, err := client.Get(ctx, resourceGroup, accountName, transformName) + if err != nil { + return fmt.Errorf("Error retrieving Transform %q from Media Services Account %q (Resource Group %q): %+v", transformName, accountName, resourceGroup, err) + } + + d.SetId(*transform.ID) + + return resourceMediaTransformRead(d, meta) +} + +func resourceMediaTransformRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Media.TransformsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.TransformID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.MediaserviceName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Transform %q was not found in Media Services Account %q and Resource Group %q - removing from state", id.Name, id.MediaserviceName, id.ResourceGroup) + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving Transform %q in Media Services Account %q (Resource Group %q): %+v", id.Name, id.MediaserviceName, id.ResourceGroup, err) + } + + d.Set("name", id.Name) + d.Set("resource_group_name", id.ResourceGroup) + d.Set("media_services_account_name", id.MediaserviceName) + + if props := resp.TransformProperties; props != nil { + if description := props.Description; description != nil { + d.Set("description", description) + } + + outputs := flattenTransformOutputs(props.Outputs) + if err := d.Set("output", outputs); err != nil { + return fmt.Errorf("Error flattening `output`: %s", err) + } + } + + return nil +} + +func resourceMediaTransformDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Media.TransformsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.TransformID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Delete(ctx, id.ResourceGroup, id.MediaserviceName, id.Name) + if err != nil { + if response.WasNotFound(resp.Response) { + return nil + } + return fmt.Errorf("Error deleting Transform %q in Media Services Account %q (Resource Group %q): %+v", id.Name, id.MediaserviceName, id.ResourceGroup, err) + } + + return nil +} + +func expandTransformOuputs(input []interface{}) (*[]media.TransformOutput, error) { + results := make([]media.TransformOutput, 0) + + for _, transformOuputRaw := range input { + transform := transformOuputRaw.(map[string]interface{}) + + preset, err := expandPreset(transform) + if err != nil { + return nil, err + } + + transformOuput := media.TransformOutput{ + Preset: preset, + } + + if transform["on_error_action"] != nil { + transformOuput.OnError = media.OnErrorType(transform["on_error_action"].(string)) + } + + if transform["relative_priority"] != nil { + transformOuput.RelativePriority = media.Priority(transform["relative_priority"].(string)) + } + + results = append(results, transformOuput) + } + + return &results, nil +} + +func flattenTransformOutputs(input *[]media.TransformOutput) []interface{} { + if input == nil { + return []interface{}{} + } + + results := make([]interface{}, 0) + for _, transformOuput := range *input { + output := make(map[string]interface{}) + output["on_error_action"] = string(transformOuput.OnError) + output["relative_priority"] = string(transformOuput.RelativePriority) + attribute, preset := flattenPreset(transformOuput.Preset) + if attribute != "" { + output[attribute] = preset + } + results = append(results, output) + } + + return results +} + +func expandPreset(transform map[string]interface{}) (media.BasicPreset, error) { + presetsCount := 0 + presetType := "" + if transform["builtin_preset"] != nil && len(transform["builtin_preset"].([]interface{})) > 0 { + presetsCount++ + presetType = string(media.OdataTypeMicrosoftMediaBuiltInStandardEncoderPreset) + } + if transform["audio_analyzer_preset"] != nil && len(transform["audio_analyzer_preset"].([]interface{})) > 0 { + presetsCount++ + presetType = string(media.OdataTypeMicrosoftMediaAudioAnalyzerPreset) + } + if transform["video_analyzer_preset"] != nil && len(transform["video_analyzer_preset"].([]interface{})) > 0 { + presetsCount++ + presetType = string(media.OdataTypeMicrosoftMediaVideoAnalyzerPreset) + } + if transform["face_detector_preset"] != nil && len(transform["face_detector_preset"].([]interface{})) > 0 { + presetsCount++ + presetType = string(media.OdataTypeMicrosoftMediaFaceDetectorPreset) + } + + if presetsCount == 0 { + return nil, fmt.Errorf("output must contain at least one type of preset: builtin_preset,face_detector_preset,video_analyzer_preset or audio_analyzer_preset.") + } + + if presetsCount > 1 { + return nil, fmt.Errorf("more than one type of preset in the same output is not allowed.") + } + + switch presetType { + case string(media.OdataTypeMicrosoftMediaBuiltInStandardEncoderPreset): + presets := transform["builtin_preset"].([]interface{}) + preset := presets[0].(map[string]interface{}) + if preset["preset_name"] == nil { + return nil, fmt.Errorf("preset_name is required for BuiltInStandardEncoderPreset") + } + presetName := preset["preset_name"].(string) + builtInPreset := &media.BuiltInStandardEncoderPreset{ + PresetName: media.EncoderNamedPreset(presetName), + OdataType: media.OdataTypeMicrosoftMediaBuiltInStandardEncoderPreset, + } + return builtInPreset, nil + case string(media.OdataTypeMicrosoftMediaAudioAnalyzerPreset): + presets := transform["audio_analyzer_preset"].([]interface{}) + preset := presets[0].(map[string]interface{}) + audioAnalyzerPreset := &media.AudioAnalyzerPreset{ + OdataType: media.OdataTypeMicrosoftMediaAudioAnalyzerPreset, + } + if preset["audio_language"] != nil && preset["audio_language"].(string) != "" { + audioAnalyzerPreset.AudioLanguage = utils.String(preset["audio_language"].(string)) + } + if preset["audio_analysis_mode"] != nil { + audioAnalyzerPreset.Mode = media.AudioAnalysisMode(preset["audio_analysis_mode"].(string)) + } + return audioAnalyzerPreset, nil + case string(media.OdataTypeMicrosoftMediaFaceDetectorPreset): + presets := transform["face_detector_preset"].([]interface{}) + preset := presets[0].(map[string]interface{}) + faceDetectorPreset := &media.FaceDetectorPreset{ + OdataType: media.OdataTypeMicrosoftMediaFaceDetectorPreset, + } + if preset["analysis_resolution"] != nil { + faceDetectorPreset.Resolution = media.AnalysisResolution(preset["analysis_resolution"].(string)) + } + return faceDetectorPreset, nil + case string(media.OdataTypeMicrosoftMediaVideoAnalyzerPreset): + presets := transform["video_analyzer_preset"].([]interface{}) + preset := presets[0].(map[string]interface{}) + videoAnalyzerPreset := &media.VideoAnalyzerPreset{ + OdataType: media.OdataTypeMicrosoftMediaVideoAnalyzerPreset, + } + if preset["audio_language"] != nil { + videoAnalyzerPreset.AudioLanguage = utils.String(preset["audio_language"].(string)) + } + if preset["audio_analysis_mode"] != nil { + videoAnalyzerPreset.Mode = media.AudioAnalysisMode(preset["audio_analysis_mode"].(string)) + } + if preset["insights_type"] != nil { + videoAnalyzerPreset.InsightsToExtract = media.InsightsType(preset["insights_type"].(string)) + } + return videoAnalyzerPreset, nil + default: + return nil, fmt.Errorf("output must contain at least one type of preset: builtin_preset,face_detector_preset,video_analyzer_preset or audio_analyzer_preset") + } +} + +func flattenPreset(preset media.BasicPreset) (string, []interface{}) { + if preset == nil { + return "", []interface{}{} + } + + results := make([]interface{}, 0) + result := make(map[string]interface{}) + switch preset.(type) { + case media.AudioAnalyzerPreset: + mediaAudioAnalyzerPreset, _ := preset.AsAudioAnalyzerPreset() + result["audio_analysis_mode"] = string(mediaAudioAnalyzerPreset.Mode) + if mediaAudioAnalyzerPreset.AudioLanguage != nil { + result["audio_language"] = mediaAudioAnalyzerPreset.AudioLanguage + } + results = append(results, result) + return "audio_analyzer_preset", results + case media.BuiltInStandardEncoderPreset: + builtInStandardEncoderPreset, _ := preset.AsBuiltInStandardEncoderPreset() + result["preset_name"] = string(builtInStandardEncoderPreset.PresetName) + results = append(results, result) + return "builtin_preset", results + case media.FaceDetectorPreset: + faceDetectorPreset, _ := preset.AsFaceDetectorPreset() + result["analysis_resolution"] = string(faceDetectorPreset.Resolution) + results = append(results, result) + return "face_detector_preset", results + case media.VideoAnalyzerPreset: + videoAnalyzerPreset, _ := preset.AsVideoAnalyzerPreset() + result["audio_analysis_mode"] = string(videoAnalyzerPreset.Mode) + result["insights_type"] = string(videoAnalyzerPreset.InsightsToExtract) + if videoAnalyzerPreset.AudioLanguage != nil { + result["audio_language"] = videoAnalyzerPreset.AudioLanguage + } + results = append(results, result) + return "video_analyzer_preset", results + } + + return "", results +} diff --git a/azurerm/internal/services/media/media_transform_resource_test.go b/azurerm/internal/services/media/media_transform_resource_test.go new file mode 100644 index 000000000000..385d395787f2 --- /dev/null +++ b/azurerm/internal/services/media/media_transform_resource_test.go @@ -0,0 +1,199 @@ +package media_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/media/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type MediaTransformResource struct { +} + +func TestAccMediaTransform_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_media_transform", "test") + r := MediaTransformResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(data.ResourceName).Key("name").HasValue("Transform-1"), + check.That(data.ResourceName).Key("output.#").HasValue("1"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccMediaTransform_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_media_transform", "test") + r := MediaTransformResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(data.ResourceName).Key("description").HasValue("Transform description"), + check.That(data.ResourceName).Key("output.#").HasValue("4"), + check.That(data.ResourceName).Key("name").HasValue("Transform-1"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccMediaTransform_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_media_transform", "test") + r := MediaTransformResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(data.ResourceName).Key("name").HasValue("Transform-1"), + check.That(data.ResourceName).Key("output.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(data.ResourceName).Key("description").HasValue("Transform description"), + check.That(data.ResourceName).Key("output.#").HasValue("4"), + check.That(data.ResourceName).Key("name").HasValue("Transform-1"), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(data.ResourceName).Key("name").HasValue("Transform-1"), + check.That(data.ResourceName).Key("output.#").HasValue("1"), + check.That(data.ResourceName).Key("description").HasValue(""), + ), + }, + data.ImportStep(), + }) +} + +func (r MediaTransformResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + id, err := parse.TransformID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.Media.TransformsClient.Get(ctx, id.ResourceGroup, id.MediaserviceName, id.Name) + if err != nil { + return nil, fmt.Errorf("retrieving Transform %s (Media Account %s) (resource group: %s): %v", id.Name, id.MediaserviceName, id.ResourceGroup, err) + } + + return utils.Bool(resp.TransformProperties != nil), nil +} + +func (r MediaTransformResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_media_transform" "test" { + name = "Transform-1" + resource_group_name = azurerm_resource_group.test.name + media_services_account_name = azurerm_media_services_account.test.name + output { + relative_priority = "High" + on_error_action = "ContinueJob" + builtin_preset { + preset_name = "AACGoodQualityAudio" + } + } +} + +`, r.template(data)) +} + +func (r MediaTransformResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_media_transform" "test" { + name = "Transform-1" + resource_group_name = azurerm_resource_group.test.name + media_services_account_name = azurerm_media_services_account.test.name + description = "Transform description" + output { + relative_priority = "High" + on_error_action = "ContinueJob" + builtin_preset { + preset_name = "AACGoodQualityAudio" + } + } + + output { + relative_priority = "High" + on_error_action = "StopProcessingJob" + audio_analyzer_preset { + audio_language = "en-US" + audio_analysis_mode = "Basic" + } + } + + output { + relative_priority = "Low" + on_error_action = "StopProcessingJob" + face_detector_preset { + analysis_resolution = "StandardDefinition" + } + } + + output { + relative_priority = "Normal" + on_error_action = "StopProcessingJob" + video_analyzer_preset { + audio_language = "en-US" + audio_analysis_mode = "Basic" + insights_type = "AllInsights" + } + } +} + +`, r.template(data)) +} + +func (r MediaTransformResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-media-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa1%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "GRS" +} + +resource "azurerm_media_services_account" "test" { + name = "acctestmsa%s" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + storage_account { + id = azurerm_storage_account.test.id + is_primary = true + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString) +} diff --git a/azurerm/internal/services/media/parse/transform.go b/azurerm/internal/services/media/parse/transform.go new file mode 100644 index 000000000000..760479ccf7da --- /dev/null +++ b/azurerm/internal/services/media/parse/transform.go @@ -0,0 +1,74 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type TransformId struct { + SubscriptionId string + ResourceGroup string + MediaserviceName string + Name string +} + +func NewTransformID(subscriptionId, resourceGroup, mediaserviceName, name string) TransformId { + return TransformId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + MediaserviceName: mediaserviceName, + Name: name, + } +} + +func (id TransformId) String() string { + segments := []string{ + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + fmt.Sprintf("Mediaservice Name %q", id.MediaserviceName), + fmt.Sprintf("Name %q", id.Name), + } + return strings.Join(segments, " / ") +} + +func (id TransformId) ID(_ string) string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Media/mediaservices/%s/transforms/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.MediaserviceName, id.Name) +} + +// TransformID parses a Transform ID into an TransformId struct +func TransformID(input string) (*TransformId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := TransformId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.MediaserviceName, err = id.PopSegment("mediaservices"); err != nil { + return nil, err + } + if resourceId.Name, err = id.PopSegment("transforms"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/azurerm/internal/services/media/parse/transform_test.go b/azurerm/internal/services/media/parse/transform_test.go new file mode 100644 index 000000000000..2c5fbfcd0777 --- /dev/null +++ b/azurerm/internal/services/media/parse/transform_test.go @@ -0,0 +1,128 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = TransformId{} + +func TestTransformIDFormatter(t *testing.T) { + actual := NewTransformID("12345678-1234-9876-4563-123456789012", "resGroup1", "account1", "transform1").ID("") + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/transform1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestTransformID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *TransformId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing MediaserviceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/", + Error: true, + }, + + { + // missing value for MediaserviceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/transform1", + Expected: &TransformId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + MediaserviceName: "account1", + Name: "transform1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.MEDIA/MEDIASERVICES/ACCOUNT1/TRANSFORMS/TRANSFORM1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := TransformID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.MediaserviceName != v.Expected.MediaserviceName { + t.Fatalf("Expected %q but got %q for MediaserviceName", v.Expected.MediaserviceName, actual.MediaserviceName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/media/registration.go b/azurerm/internal/services/media/registration.go index cddc8763b4be..707fb9994568 100644 --- a/azurerm/internal/services/media/registration.go +++ b/azurerm/internal/services/media/registration.go @@ -28,5 +28,6 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_media_services_account": resourceMediaServicesAccount(), "azurerm_media_asset": resourceMediaAsset(), + "azurerm_media_transform": resourceMediaTransform(), } } diff --git a/azurerm/internal/services/media/resourceids.go b/azurerm/internal/services/media/resourceids.go index a6b6a701e49f..2ebdb0ed34aa 100644 --- a/azurerm/internal/services/media/resourceids.go +++ b/azurerm/internal/services/media/resourceids.go @@ -1,4 +1,5 @@ package media //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=MediaService -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Transform -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/transform1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Asset -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/assets/asset1 diff --git a/azurerm/internal/services/media/validate/transform_id.go b/azurerm/internal/services/media/validate/transform_id.go new file mode 100644 index 000000000000..1755cc5f65ad --- /dev/null +++ b/azurerm/internal/services/media/validate/transform_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/media/parse" +) + +func TransformID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.TransformID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/azurerm/internal/services/media/validate/transform_id_test.go b/azurerm/internal/services/media/validate/transform_id_test.go new file mode 100644 index 000000000000..478094b07966 --- /dev/null +++ b/azurerm/internal/services/media/validate/transform_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestTransformID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing MediaserviceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/", + Valid: false, + }, + + { + // missing value for MediaserviceName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Media/mediaservices/account1/transforms/transform1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.MEDIA/MEDIASERVICES/ACCOUNT1/TRANSFORMS/TRANSFORM1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := TransformID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index cd08fdde7a73..2893427db9c0 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -2190,6 +2190,9 @@
  • azurerm_media_services_account
  • +
  • + azurerm_media_transform +
  • diff --git a/website/docs/r/media_transform.html.markdown b/website/docs/r/media_transform.html.markdown new file mode 100644 index 000000000000..ef52f66257cc --- /dev/null +++ b/website/docs/r/media_transform.html.markdown @@ -0,0 +1,201 @@ +--- +subcategory: "Media" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_media_transform" +description: |- + Manages a Transform. +--- + +# azurerm_media_transform + +Manages a Transform. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "media-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "examplestoracc" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "GRS" +} + +resource "azurerm_media_services_account" "example" { + name = "examplemediaacc" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + storage_account { + id = azurerm_storage_account.example.id + is_primary = true + } +} + +resource "azurerm_media_transform" "example" { + name = "transform1" + resource_group_name = azurerm_resource_group.example.name + media_services_account_name = azurerm_media_services_account.example.name + description = "My transform description" + output { + relative_priority = "Normal" + on_error_action = "ContinueJob" + builtin_preset { + preset_name = "AACGoodQualityAudio" + } + } +} + +``` + +## Example Usage with Multiple Outputs + +```hcl +resource "azurerm_resource_group" "example" { + name = "media-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "examplestoracc" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "GRS" +} + +resource "azurerm_media_services_account" "example" { + name = "examplemediaacc" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + storage_account { + id = azurerm_storage_account.example.id + is_primary = true + } +} + +resource "azurerm_media_transform" "example" { + name = "transform1" + resource_group_name = azurerm_resource_group.example.name + media_services_account_name = azurerm_media_services_account.example.name + description = "My transform description" + output { + relative_priority = "Normal" + on_error_action = "ContinueJob" + builtin_preset { + preset_name = "AACGoodQualityAudio" + } + } + + output { + relative_priority = "Low" + on_error_action = "ContinueJob" + audio_analyzer_preset { + audio_language = "en-US" + audio_analysis_mode = "Basic" + } + } + + output { + relative_priority = "Low" + on_error_action = "StopProcessingJob" + face_detector_preset { + analysis_resolution = "StandardDefinition" + } + } +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `media_services_account_name` - (Required) The Media Services account name. Changing this forces a new Transform to be created. + +* `name` - (Required) The name which should be used for this Transform. Changing this forces a new Transform to be created. + +* `resource_group_name` - (Required) The name of the Resource Group where the Transform should exist. Changing this forces a new Transform to be created. + +--- + +* `description` - (Optional) An optional verbose description of the Transform. + +* `output` - (Required) One or more `output` blocks as defined below. At least one `output` must be defined. + +--- + +A `output` block supports the following: + +* `audio_analyzer_preset` - (Optional) A `audio_analyzer_preset` block as defined below. + +* `builtin_preset` - (Optional) A `builtin_preset` block as defined below. + +* `face_detector_preset` - (Optional) A `face_detector_preset` block as defined below. + +* `on_error_action` - (Optional) A Transform can define more than one outputs. This property defines what the service should do when one output fails - either continue to produce other outputs, or, stop the other outputs. The overall Job state will not reflect failures of outputs that are specified with `ContinueJob`. Possibles value are `StopProcessingJob` or `ContinueJob`. + +* `relative_priority` - (Optional) Sets the relative priority of the TransformOutputs within a Transform. This sets the priority that the service uses for processing Transform Outputs. Possibles value are `High`, `Normal` or `Low`. + +* `video_analyzer_preset` - (Optional) A `video_analyzer_preset` block as defined below. + +-> **NOTE:** Each output can only have one type of preset: builtin_preset,audio_analyzer_preset,face_detector_preset or video_analyzer_preset. If you need to apply differents presets you must create one output for each one. + +--- + +A `builtin_preset` block supports the following: + +* `preset_name` - (Optional) The built-in preset to be used for encoding videos. The allowed values are `AACGoodQualityAudio`, `AdaptiveStreaming`,`ContentAwareEncoding`, `ContentAwareEncodingExperimental`,`CopyAllBitrateNonInterleaved`, `H264MultipleBitrate1080p`,`H264MultipleBitrate720p`, `H264MultipleBitrateSD`,`H264SingleBitrate1080p`, `H264SingleBitrate720p` and `H264SingleBitrateSD`. + +--- + +A `audio_analyzer_preset` block supports the following: + +* `audio_language` - (Optional) The language for the audio payload in the input using the BCP-47 format of 'language tag-region' (e.g: 'en-US'). If you know the language of your content, it is recommended that you specify it. The language must be specified explicitly for AudioAnalysisMode:Basic, since automatic language detection is not included in basic mode. If the language isn't specified, automatic language detection will choose the first language detected and process with the selected language for the duration of the file. It does not currently support dynamically switching between languages after the first language is detected. The automatic detection works best with audio recordings with clearly discernable speech. If automatic detection fails to find the language, transcription would fallback to 'en-US'." The list of supported languages is available here: https://go.microsoft.com/fwlink/?linkid=2109463. + +* `audio_analysis_mode` - (Optional) Possibles value are `Basic` or `Standard`. Determines the set of audio analysis operations to be performed. + +--- + +A `video_analyzer_preset` block supports the following: + +* `audio_language` - (Optional) The language for the audio payload in the input using the BCP-47 format of 'language tag-region' (e.g: 'en-US'). If you know the language of your content, it is recommended that you specify it. The language must be specified explicitly for AudioAnalysisMode:Basic, since automatic language detection is not included in basic mode. If the language isn't specified, automatic language detection will choose the first language detected and process with the selected language for the duration of the file. It does not currently support dynamically switching between languages after the first language is detected. The automatic detection works best with audio recordings with clearly discernable speech. If automatic detection fails to find the language, transcription would fallback to 'en-US'." The list of supported languages is available here: https://go.microsoft.com/fwlink/?linkid=2109463. + +* `audio_analysis_mode` - (Optional) Possibles value are `Basic` or `Standard`. Determines the set of audio analysis operations to be performed. + +* `insights_type` - (Optional) Defines the type of insights that you want the service to generate. The allowed values are `AudioInsightsOnly`, `VideoInsightsOnly`, and `AllInsights`. If you set this to `AllInsights` and the input is audio only, then only audio insights are generated. Similarly if the input is video only, then only video insights are generated. It is recommended that you not use `AudioInsightsOnly` if you expect some of your inputs to be video only; or use `VideoInsightsOnly` if you expect some of your inputs to be audio only. Your Jobs in such conditions would error out. + +--- + +A `face_detector_preset` block supports the following: + +* `analysis_resolution` - (Optional) Possibles value are `SourceResolution` or `StandardDefinition`. Specifies the maximum resolution at which your video is analyzed. The default behavior is `SourceResolution` which will keep the input video at its original resolution when analyzed. Using `StandardDefinition` will resize input videos to standard definition while preserving the appropriate aspect ratio. It will only resize if the video is of higher resolution. For example, a 1920x1080 input would be scaled to 640x360 before processing. Switching to `StandardDefinition` will reduce the time it takes to process high resolution video. It may also reduce the cost of using this component (see https://azure.microsoft.com/en-us/pricing/details/media-services/#analytics for details). However, faces that end up being too small in the resized video may not be detected. + + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Transform. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Transform. +* `read` - (Defaults to 5 minutes) Used when retrieving the Transform. +* `update` - (Defaults to 30 minutes) Used when updating the Transform. +* `delete` - (Defaults to 30 minutes) Used when deleting the Transform. + +## Import + +Transforms can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_media_transform.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Media/mediaservices/media1/transforms/transform1 +```