diff --git a/assets/terraform/examples/data-sources/panos_predefined_dlp_file_type/data-source.tf b/assets/terraform/examples/data-sources/panos_predefined_dlp_file_type/data-source.tf new file mode 100644 index 00000000..0fbf5ff7 --- /dev/null +++ b/assets/terraform/examples/data-sources/panos_predefined_dlp_file_type/data-source.tf @@ -0,0 +1,80 @@ +# Look up predefined DLP file type properties for use with custom data patterns. +# +# Each predefined file type (pdf, rtf, docx, etc.) has file properties with +# internal names (e.g. "panav-rsp-rtf-dlp-keywords") and human-readable labels +# (e.g. "Keywords/Tags"). When configuring panos_custom_data_object resources +# with file-properties pattern types, the file_property field requires the +# internal name — use this data source to resolve it from the label. + +# Define custom data pattern input — users typically pass this as a variable. +# The file_property label is the human-readable name shown in the PAN-OS UI. +variable "custom_data_pattern" { + default = { + name = "example-pattern" + file_property = [ + { + name = "keyword-check" + file_type = "rtf" + file_property = "Keywords/Tags" + property_value = "confidential" + }, + { + name = "author-check" + file_type = "pdf" + file_property = "Author" + property_value = "admin" + }, + ] + } +} + +# Look up each file type referenced in the pattern. +data "panos_predefined_dlp_file_type" "rtf" { + location = { predefined = {} } + name = "rtf" +} + +data "panos_predefined_dlp_file_type" "pdf" { + location = { predefined = {} } + name = "pdf" +} + +locals { + # Map file_type -> properties list for easy lookup + dlp_file_types = { + "rtf" = data.panos_predefined_dlp_file_type.rtf.file_property + "pdf" = data.panos_predefined_dlp_file_type.pdf.file_property + } + + # Resolve each label to its internal property name + resolved = [ + for fp in var.custom_data_pattern.file_property : { + name = fp.name + file_type = fp.file_type + file_property = one([ + for p in local.dlp_file_types[fp.file_type] + : p.name if p.label == fp.file_property + ]) + property_value = fp.property_value + } + ] +} + +# Use the resolved properties in a custom data object. +resource "panos_custom_data_object" "example" { + location = { shared = {} } + name = var.custom_data_pattern.name + + pattern_type = { + file_properties = { + pattern = [ + for fp in local.resolved : { + name = fp.name + file_type = fp.file_type + file_property = fp.file_property + property_value = fp.property_value + } + ] + } + } +} diff --git a/assets/terraform/test/datasource_predefined_dlp_file_type_test.go b/assets/terraform/test/datasource_predefined_dlp_file_type_test.go new file mode 100644 index 00000000..76dbac29 --- /dev/null +++ b/assets/terraform/test/datasource_predefined_dlp_file_type_test.go @@ -0,0 +1,364 @@ +package provider_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// TestAccPredefinedDlpFileType_Basic reads a well-known predefined DLP file type +// ("pdf") and verifies that its name and file_property list are correctly populated. +// The "pdf" entry is guaranteed to exist on PAN-OS devices and its file-property +// entries (e.g. panav-rsp-pdf-dlp-author) have stable, device-supplied labels. +// ListPartial with index 0 asserts the list is non-empty and the first element is +// a well-formed object; exact entry names are not pinned because ordering may vary +// across device versions. +func TestAccPredefinedDlpFileType_Basic(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: predefinedDlpFileType_Basic_Tmpl, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("name"), + knownvalue.StringExact("pdf"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("file_property"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.NotNull(), + "label": knownvalue.NotNull(), + }), + }), + ), + }, + }, + }, + }) +} + +// TestAccPredefinedDlpFileType_FileProperties verifies that the file_property +// sub-entries for the "pdf" file type are read with both name and label fields +// populated. The full, ordered list of file-property sub-entries is asserted +// against the values returned by Panorama 11.2. The device returns 8 properties +// for "pdf" in the order: title, author, subject, comments, keywords, +// titus-corp-sensitivity, titus-corp-classification, titus-GUID. +func TestAccPredefinedDlpFileType_FileProperties(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: predefinedDlpFileType_FileProperties_Tmpl, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("name"), + knownvalue.StringExact("pdf"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("file_property"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-title"), + "label": knownvalue.StringExact("Title"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-author"), + "label": knownvalue.StringExact("Author"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-subject"), + "label": knownvalue.StringExact("Subject"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-comments"), + "label": knownvalue.StringExact("Comments"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-keywords"), + "label": knownvalue.StringExact("Keywords"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-titus-corp-sensitivity"), + "label": knownvalue.StringExact("Sensitivity"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-titus-corp-classification"), + "label": knownvalue.StringExact("Classification"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("panav-rsp-pdf-dlp-titus-GUID"), + "label": knownvalue.StringExact("TITUS GUID"), + }), + }), + ), + }, + }, + }, + }) +} + +// TestAccPredefinedDlpFileType_MultipleEntries reads several well-known predefined +// DLP file types in a single Terraform configuration and verifies that each one +// resolves independently with the correct name. This exercises the data source +// for the full set of known entries (docx, pptx, xlsx, pdf) and confirms that +// multiple data source instances can coexist in one plan. +func TestAccPredefinedDlpFileType_MultipleEntries(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: predefinedDlpFileType_MultipleEntries_Tmpl, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.docx", + tfjsonpath.New("name"), + knownvalue.StringExact("docx"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.docx", + tfjsonpath.New("file_property"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.NotNull(), + "label": knownvalue.NotNull(), + }), + }), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pptx", + tfjsonpath.New("name"), + knownvalue.StringExact("pptx"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pptx", + tfjsonpath.New("file_property"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.NotNull(), + "label": knownvalue.NotNull(), + }), + }), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.xlsx", + tfjsonpath.New("name"), + knownvalue.StringExact("xlsx"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.xlsx", + tfjsonpath.New("file_property"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.NotNull(), + "label": knownvalue.NotNull(), + }), + }), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("name"), + knownvalue.StringExact("pdf"), + ), + statecheck.ExpectKnownValue( + "data.panos_predefined_dlp_file_type.pdf", + tfjsonpath.New("file_property"), + knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.NotNull(), + "label": knownvalue.NotNull(), + }), + }), + ), + }, + }, + }, + }) +} + +// predefinedDlpFileType_Basic_Tmpl reads the "pdf" predefined DLP file type +// from the device using the predefined location. No supporting resources are +// required because the entry is device-supplied and read-only. +const predefinedDlpFileType_Basic_Tmpl = ` +data "panos_predefined_dlp_file_type" "pdf" { + location = { predefined = {} } + name = "pdf" +} +` + +// predefinedDlpFileType_FileProperties_Tmpl reads "pdf" without specifying an +// explicit file_property list so the provider returns all entries from the device +// in the server-defined order. The ListExact assertion in the test captures the +// full set of 8 properties returned by Panorama 11.2. +const predefinedDlpFileType_FileProperties_Tmpl = ` +data "panos_predefined_dlp_file_type" "pdf" { + location = { predefined = {} } + name = "pdf" +} +` + +// TestAccPredefinedDlpFileType_CustomDataPatternLookup mirrors the v1 provider +// pattern where a user defines custom data patterns with file_property entries +// that reference file types by name and properties by human-readable label: +// +// custom_data_pattern: +// - name: "test3" +// type: "file-properties" +// file_property: +// - name: "blah2" +// file_type: "rtf" +// file_property: "Keywords/Tags" +// property_value: "foo" +// +// The test uses a local variable to model this input, looks up each unique file +// type via the data source, resolves label → internal property name in locals, +// and then constructs the final resolved list that would feed into a +// panos_custom_data_object resource. Outputs verify the resolved values. +func TestAccPredefinedDlpFileType_CustomDataPatternLookup(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: predefinedDlpFileType_CustomDataPatternLookup_Tmpl, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue( + "resolved_file_properties", + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("blah2"), + "file_type": knownvalue.StringExact("rtf"), + "file_property": knownvalue.StringExact("panav-rsp-rtf-dlp-keywords"), + "property_value": knownvalue.StringExact("foo"), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("blah3"), + "file_type": knownvalue.StringExact("pdf"), + "file_property": knownvalue.StringExact("panav-rsp-pdf-dlp-author"), + "property_value": knownvalue.StringExact("bar"), + }), + }), + ), + }, + }, + }, + }) +} + +// predefinedDlpFileType_CustomDataPatternLookup_Tmpl models the full v1 workflow: +// +// 1. A local variable defines custom data pattern entries (mimicking user input) +// 2. Data sources look up each unique file type +// 3. A local resolves each entry's label to the internal property name +// 4. The resolved list is output — in production it would feed into +// panos_custom_data_object file_properties pattern entries +const predefinedDlpFileType_CustomDataPatternLookup_Tmpl = ` +# Step 1: User-defined custom data pattern input (mirrors v1 variable structure) +locals { + custom_data_pattern = { + name = "test3" + type = "file-properties" + file_property = [ + { + name = "blah2" + file_type = "rtf" + file_property = "Keywords/Tags" + property_value = "foo" + }, + { + name = "blah3" + file_type = "pdf" + file_property = "Author" + property_value = "bar" + }, + ] + } + + # Collect unique file types from the input + unique_file_types = distinct([ + for fp in local.custom_data_pattern.file_property : fp.file_type + ]) +} + +# Step 2: Look up each unique file type (one data source per file type) +data "panos_predefined_dlp_file_type" "lookup_rtf" { + location = { predefined = {} } + name = "rtf" +} + +data "panos_predefined_dlp_file_type" "lookup_pdf" { + location = { predefined = {} } + name = "pdf" +} + +locals { + # Build a map of file_type -> (label -> internal_name) for easy lookup + file_type_data = { + "rtf" = data.panos_predefined_dlp_file_type.lookup_rtf.file_property + "pdf" = data.panos_predefined_dlp_file_type.lookup_pdf.file_property + } + + # Step 3: Resolve each file_property entry's label to internal property name + resolved_file_properties = [ + for fp in local.custom_data_pattern.file_property : { + name = fp.name + file_type = fp.file_type + file_property = one([ + for p in local.file_type_data[fp.file_type] + : p.name if p.label == fp.file_property + ]) + property_value = fp.property_value + } + ] +} + +# Step 4: Output the resolved list (in production, this feeds into +# panos_custom_data_object pattern_type.file_properties.pattern entries) +output "resolved_file_properties" { + value = local.resolved_file_properties +} +` + +// predefinedDlpFileType_MultipleEntries_Tmpl reads the four known file types +// (docx, pptx, xlsx, pdf) in one configuration to exercise independent data +// source instances sharing the same predefined location. +const predefinedDlpFileType_MultipleEntries_Tmpl = ` +data "panos_predefined_dlp_file_type" "docx" { + location = { predefined = {} } + name = "docx" +} + +data "panos_predefined_dlp_file_type" "pptx" { + location = { predefined = {} } + name = "pptx" +} + +data "panos_predefined_dlp_file_type" "xlsx" { + location = { predefined = {} } + name = "xlsx" +} + +data "panos_predefined_dlp_file_type" "pdf" { + location = { predefined = {} } + name = "pdf" +} +` diff --git a/pkg/commands/codegen/codegen.go b/pkg/commands/codegen/codegen.go index 900e76ca..64252dea 100644 --- a/pkg/commands/codegen/codegen.go +++ b/pkg/commands/codegen/codegen.go @@ -67,13 +67,14 @@ func deriveSubcategoryFromPath(specPath string) string { // Map directory names to subcategory names subcategoryMap := map[string]string{ - "network": "Network", - "objects": "Objects", - "device": "Device", - "panorama": "Panorama", - "policies": "Policies", - "actions": "", // empty for actions - "schema": "", // empty for schema + "network": "Network", + "objects": "Objects", + "device": "Device", + "panorama": "Panorama", + "policies": "Policies", + "predefined": "Predefined", + "actions": "", // empty for actions + "schema": "", // empty for schema } if subcategory, ok := subcategoryMap[category]; ok { diff --git a/pkg/properties/normalized.go b/pkg/properties/normalized.go index d545c7d4..93fcc7d5 100644 --- a/pkg/properties/normalized.go +++ b/pkg/properties/normalized.go @@ -899,6 +899,29 @@ func (spec *Normalization) SupportedMethod(method object.GoSdkMethod) bool { return slices.Contains(spec.GoSdkSupportedMethods, method) } +// HasLocationVars returns true if any location has variables defined. +func (spec *Normalization) HasLocationVars() bool { + for _, location := range spec.Locations { + if len(location.Vars) > 0 { + return true + } + } + return false +} + +// HasLocationEntryXpathVars returns true if any location has xpath elements +// that contain "Entry", which means util.AsEntryXpath() is needed. +func (spec *Normalization) HasLocationEntryXpathVars() bool { + for _, location := range spec.Locations { + for _, xpath := range location.Xpath { + if strings.Contains(xpath, "Entry") { + return true + } + } + } + return false +} + func (spec *Normalization) OrderedLocations() []*Location { elements := make([]*Location, len(spec.Locations)) for _, elt := range spec.Locations { diff --git a/pkg/translate/imports.go b/pkg/translate/imports.go index fcd3529d..9f4d1157 100644 --- a/pkg/translate/imports.go +++ b/pkg/translate/imports.go @@ -33,7 +33,9 @@ func RenderImports(spec *properties.Normalization, templateTypes ...string) (str case "location": manager.AddStandardImport("fmt", "") manager.AddSdkImport("github.com/PaloAltoNetworks/pango/errors", "") - manager.AddSdkImport("github.com/PaloAltoNetworks/pango/util", "") + if spec.HasLocationEntryXpathVars() { + manager.AddSdkImport("github.com/PaloAltoNetworks/pango/util", "") + } manager.AddSdkImport("github.com/PaloAltoNetworks/pango/version", "") if spec.ResourceXpathVariablesWithChecks(true) { manager.AddStandardImport("strings", "") diff --git a/pkg/translate/imports_test.go b/pkg/translate/imports_test.go index f9f81e1b..f2b966e4 100644 --- a/pkg/translate/imports_test.go +++ b/pkg/translate/imports_test.go @@ -9,7 +9,31 @@ import ( ) func TestRenderImports(t *testing.T) { - // given + // given - no location entry xpath vars, util should not be imported + expectedImports := ` +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/version" +)` + + // when + spec := &properties.Normalization{ + PanosXpath: properties.PanosXpath{ + Path: []string{"test"}, + }, + } + + actualImports, _ := RenderImports(spec, "location") + + // then + assert.NotNil(t, actualImports) + assert.Equal(t, expectedImports, actualImports) +} + +func TestRenderImportsWithEntryXpathVars(t *testing.T) { + // given - location has entry xpath vars, util should be imported expectedImports := ` import ( "fmt" @@ -24,6 +48,11 @@ import ( PanosXpath: properties.PanosXpath{ Path: []string{"test"}, }, + Locations: map[string]*properties.Location{ + "vsys": { + Xpath: []string{"config", "devices", "Entry", "vsys", "Entry"}, + }, + }, } actualImports, _ := RenderImports(spec, "location") diff --git a/pkg/translate/terraform_provider/entity_generators.go b/pkg/translate/terraform_provider/entity_generators.go index 9ff4bfba..2c7cc2f9 100644 --- a/pkg/translate/terraform_provider/entity_generators.go +++ b/pkg/translate/terraform_provider/entity_generators.go @@ -325,7 +325,9 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(resourceTyp proper if len(spec.Locations) > 0 { terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/diag", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + if spec.HasLocationVars() { + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + } if resourceTyp != properties.ResourceCustom { terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-log/tflog", "") } @@ -373,7 +375,9 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(resourceTyp proper if len(spec.Locations) > 0 { terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/diag", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + if spec.HasLocationVars() { + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + } if resourceTyp != properties.ResourceCustom { terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-log/tflog", "") } @@ -468,7 +472,9 @@ func (g *GenerateTerraformProvider) GenerateTerraformDataSource(resourceTyp prop terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/attr", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/diag", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema", "rsschema") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + if spec.HasLocationVars() { + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") + } names := NewNameProvider(spec, resourceTyp) funcMap := template.FuncMap{ @@ -556,7 +562,9 @@ func (g *GenerateTerraformProvider) GenerateCommonCode(resourceTyp properties.Re terraformProvider.ImportManager.AddStandardImport("encoding/base64", "") } terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/types/basetypes", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/path", "") + if !spec.TerraformProviderConfig.SkipResource { + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/path", "") + } case properties.ResourceConfig: terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/types/basetypes", "") case properties.ResourceCustom: diff --git a/specs/predefined/dlp-file-type.yaml b/specs/predefined/dlp-file-type.yaml new file mode 100644 index 00000000..e7912276 --- /dev/null +++ b/specs/predefined/dlp-file-type.yaml @@ -0,0 +1,73 @@ +name: Predefined Dlp File Type +terraform_provider_config: + description: Predefined DLP file types and their associated file properties + skip_resource: true + skip_datasource: false + resource_type: entry + resource_variants: [] + suffix: predefined_dlp_file_type + plural_suffix: '' + plural_name: '' + plural_description: '' + custom_validation: false +go_sdk_config: + skip: false + package: + - predefined + - dlp + - file_type + supported_methods: + - read + - list +panos_xpath: + path: + - file-type + vars: [] +locations: +- name: predefined + xpath: + path: + - config + - predefined + - dlp-file-property + vars: [] + description: Predefined DLP file types on the device + devices: + - ngfw + - panorama + validators: [] + required: false + read_only: true +entries: +- name: name + description: '' + validators: [] +spec: + params: + - name: file-property + type: list + profiles: + - xpath: + - file-property + - entry + type: entry + validators: [] + spec: + type: object + items: + type: object + spec: + params: + - name: label + type: string + profiles: + - xpath: + - label + validators: [] + spec: {} + description: The label of the file property + required: false + variants: [] + description: List of file properties associated with this file type + required: false + variants: []