From b7381a61effb1dc12e220b0cf191df2dc96ae93a Mon Sep 17 00:00:00 2001 From: Zhenhua Li Date: Tue, 3 Sep 2024 14:23:26 -0700 Subject: [PATCH] Add go yaml validations (part 1) (#11621) --- mmv1/api/async.go | 71 +++++------------------- mmv1/api/product.go | 51 +++++++++-------- mmv1/api/product/version.go | 16 ++++-- mmv1/api/resource.go | 82 ++++++++++++++++++++++++---- mmv1/api/resource/custom_code.go | 21 ------- mmv1/api/resource/docs.go | 9 --- mmv1/api/resource/examples.go | 37 +++++++++++-- mmv1/api/resource/iam_policy.go | 24 ++++++-- mmv1/api/resource/nested_query.go | 14 ++--- mmv1/api/resource/reference_links.go | 10 ---- mmv1/api/resource/sweeper.go | 6 -- mmv1/api/resource/validation.go | 7 --- mmv1/api/timeouts.go | 8 --- mmv1/api/type.go | 22 ++++++++ mmv1/go.mod | 2 +- mmv1/go.sum | 4 +- mmv1/google/yaml_validator.go | 4 +- 17 files changed, 204 insertions(+), 184 deletions(-) diff --git a/mmv1/api/async.go b/mmv1/api/async.go index dd591c87d111..0905853e80a1 100644 --- a/mmv1/api/async.go +++ b/mmv1/api/async.go @@ -14,11 +14,11 @@ package api import ( + "log" "strings" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "golang.org/x/exp/slices" - "gopkg.in/yaml.v3" ) // Base class from which other Async classes can inherit. @@ -40,13 +40,6 @@ type Async struct { PollAsync `yaml:",inline"` } -// def validate -// super - -// check :operation, type: Operation -// check :actions, default: %w[create delete update], type: ::Array, item_type: ::String -// end - // def allow?(method) func (a Async) Allow(method string) bool { return slices.Contains(a.Actions, strings.ToLower(method)) @@ -80,11 +73,6 @@ func NewAsync() *Async { return oa } -// def validate -// super -// check :resource_inside_response, type: :boolean, default: false -// end - // Represents an asynchronous operation definition type OpAsync struct { Result OpAsyncResult @@ -106,17 +94,6 @@ type OpAsync struct { // @error = error // end -// def validate -// super - -// check :operation, type: Operation, required: true -// check :result, type: Result, default: Result.new -// check :status, type: Status -// check :error, type: Error -// check :actions, default: %w[create delete update], type: ::Array, item_type: ::String -// check :include_project, type: :boolean, default: false -// end - type OpAsyncOperation struct { Kind string @@ -156,12 +133,6 @@ type OpAsyncResult struct { // @resource_inside_response = resource_inside_response // end -// def validate -// super - -// check :path, type: String -// end - // Provides information to parse the result response to check operation // status type OpAsyncStatus struct { @@ -181,12 +152,6 @@ type OpAsyncStatus struct { // @allowed = allowed // end -// def validate -// super -// check :path, type: String -// check :allowed, type: Array, item_type: [::String, :boolean] -// end - // Provides information on how to retrieve errors of the executed operations type OpAsyncError struct { google.YamlValidator @@ -202,12 +167,6 @@ type OpAsyncError struct { // @message = message // end -// def validate -// super -// check :path, type: String -// check :message, type: String -// end - // Async implementation for polling in Terraform type PollAsync struct { // Details how to poll for an eventually-consistent resource state. @@ -233,12 +192,12 @@ type PollAsync struct { TargetOccurrences int `yaml:"target_occurrences"` } -func (a *Async) UnmarshalYAML(n *yaml.Node) error { +func (a *Async) UnmarshalYAML(unmarshal func(any) error) error { a.Actions = []string{"create", "delete", "update"} type asyncAlias Async aliasObj := (*asyncAlias)(a) - err := n.Decode(&aliasObj) + err := unmarshal(aliasObj) if err != nil { return err } @@ -250,16 +209,14 @@ func (a *Async) UnmarshalYAML(n *yaml.Node) error { return nil } -// return nil -// } - -// def validate -// super - -// check :check_response_func_existence, type: String, required: true -// check :check_response_func_absence, type: String, -// default: 'transport_tpg.PollCheckForAbsence' -// check :custom_poll_read, type: String -// check :suppress_error, type: :boolean, default: false -// check :target_occurrences, type: Integer, default: 1 -// end +func (a *Async) Validate() { + if a.Type == "OpAsync" { + if a.Operation == nil { + log.Fatalf("Missing `Operation` for OpAsync") + } else { + if a.Operation.BaseUrl != "" && a.Operation.FullUrl != "" { + log.Fatalf("`base_url` and `full_url` cannot be set at the same time in OpAsync operation.") + } + } + } +} diff --git a/mmv1/api/product.go b/mmv1/api/product.go index d02a5ac424d2..f157c057d802 100644 --- a/mmv1/api/product.go +++ b/mmv1/api/product.go @@ -16,8 +16,7 @@ package api import ( "log" "strings" - - "gopkg.in/yaml.v3" + "unicode" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" @@ -68,12 +67,11 @@ type Product struct { ClientName string `yaml:"client_name"` } -func (p *Product) UnmarshalYAML(n *yaml.Node) error { +func (p *Product) UnmarshalYAML(unmarshal func(any) error) error { type productAlias Product aliasObj := (*productAlias)(p) - err := n.Decode(&aliasObj) - if err != nil { + if err := unmarshal(aliasObj); err != nil { return err } @@ -84,31 +82,32 @@ func (p *Product) UnmarshalYAML(n *yaml.Node) error { } func (p *Product) Validate() { - // TODO Q2 Rewrite super - // super -} - -// def validate -// super -// set_variables @objects, :__product + // product names must start with a capital + for i, ch := range p.Name { + if !unicode.IsUpper(ch) { + log.Fatalf("product name `%s` must start with a capital letter.", p.Name) + } + if i == 0 { + break + } + } -// // name comes from Named, and product names must start with a capital -// caps = ('A'..'Z').to_a -// unless caps.include? @name[0] -// raise "product name `//{@name}` must start with a capital letter." -// end + if len(p.Scopes) == 0 { + log.Fatalf("Missing `scopes` for product %s", p.Name) + } -// check :display_name, type: String -// check :objects, type: Array, item_type: Api::Resource -// check :scopes, type: Array, item_type: String, required: true -// check :operation_retry, type: String + if p.Versions == nil { + log.Fatalf("Missing `versions` for product %s", p.Name) + } -// check :async, type: Api::Async -// check :legacy_name, type: String -// check :client_name, type: String + for _, v := range p.Versions { + v.Validate(p.Name) + } -// check :versions, type: Array, item_type: Api::Product::Version, required: true -// end + if p.Async != nil { + p.Async.Validate() + } +} // ==================== // Custom Setters diff --git a/mmv1/api/product/version.go b/mmv1/api/product/version.go index aa5bdd335c10..c6f3b14f884e 100644 --- a/mmv1/api/product/version.go +++ b/mmv1/api/product/version.go @@ -14,6 +14,8 @@ package product import ( + "log" + "golang.org/x/exp/slices" ) @@ -40,12 +42,14 @@ type Version struct { Name string } -// def validate -// super -// check :cai_base_url, type: String, required: false -// check :base_url, type: String, required: true -// check :name, type: String, allowed: ORDER, required: true -// end +func (v *Version) Validate(pName string) { + if v.Name == "" { + log.Fatalf("Missing `name` in `version` for product %s", pName) + } + if v.BaseUrl == "" { + log.Fatalf("Missing `base_url` in `version` for product %s", pName) + } +} // def to_s // "//{name}: //{base_url}" diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index f45908b8da70..cb1ab7daa791 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -14,6 +14,7 @@ package api import ( "fmt" + "log" "maps" "regexp" "sort" @@ -23,7 +24,6 @@ import ( "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/resource" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "golang.org/x/exp/slices" - "gopkg.in/yaml.v3" ) type Resource struct { @@ -311,7 +311,7 @@ type Resource struct { ImportPath string } -func (r *Resource) UnmarshalYAML(n *yaml.Node) error { +func (r *Resource) UnmarshalYAML(unmarshal func(any) error) error { r.CreateVerb = "POST" r.ReadVerb = "GET" r.DeleteVerb = "DELETE" @@ -320,7 +320,7 @@ func (r *Resource) UnmarshalYAML(n *yaml.Node) error { type resourceAlias Resource aliasObj := (*resourceAlias)(r) - err := n.Decode(&aliasObj) + err := unmarshal(aliasObj) if err != nil { return err } @@ -331,6 +331,9 @@ func (r *Resource) UnmarshalYAML(n *yaml.Node) error { if r.CollectionUrlKey == "" { r.CollectionUrlKey = google.Camelize(google.Plural(r.Name), "lower") } + if r.IdFormat == "" { + r.IdFormat = r.SelfLinkUri() + } if len(r.VirtualFields) > 0 { for _, f := range r.VirtualFields { @@ -341,19 +344,76 @@ func (r *Resource) UnmarshalYAML(n *yaml.Node) error { return nil } -// TODO: rewrite functions -func (r *Resource) Validate() { - // TODO Q1 Rewrite super - // super -} - func (r *Resource) SetDefault(product *Product) { r.ProductMetadata = product for _, property := range r.AllProperties() { property.SetDefault(r) } - if r.IdFormat == "" { - r.IdFormat = r.SelfLinkUri() +} + +func (r *Resource) Validate() { + if r.NestedQuery != nil && r.NestedQuery.IsListOfIds && len(r.Identity) != 1 { + log.Fatalf("`is_list_of_ids: true` implies resource has exactly one `identity` property") + } + + // Ensures we have all properties defined + for _, i := range r.Identity { + hasIdentify := slices.ContainsFunc(r.AllUserProperties(), func(p *Type) bool { + return p.Name == i + }) + if !hasIdentify { + log.Fatalf("Missing property/parameter for identity %s", i) + } + } + + if r.Description == "" { + log.Fatalf("Missing `description` for resource %s", r.Name) + } + + if !r.Exclude { + if len(r.Properties) == 0 { + log.Fatalf("Missing `properties` for resource %s", r.Name) + } + } + + allowed := []string{"POST", "PUT", "PATCH"} + if !slices.Contains(allowed, r.CreateVerb) { + log.Fatalf("Value on `create_verb` should be one of %#v", allowed) + } + + allowed = []string{"GET", "POST"} + if !slices.Contains(allowed, r.ReadVerb) { + log.Fatalf("Value on `read_verb` should be one of %#v", allowed) + } + + allowed = []string{"POST", "PUT", "PATCH", "DELETE"} + if !slices.Contains(allowed, r.DeleteVerb) { + log.Fatalf("Value on `delete_verb` should be one of %#v", allowed) + } + + allowed = []string{"POST", "PUT", "PATCH"} + if !slices.Contains(allowed, r.UpdateVerb) { + log.Fatalf("Value on `update_verb` should be one of %#v", allowed) + } + + for _, property := range r.AllProperties() { + property.Validate(r.Name) + } + + if r.IamPolicy != nil { + r.IamPolicy.Validate(r.Name) + } + + if r.NestedQuery != nil { + r.NestedQuery.Validate(r.Name) + } + + for _, example := range r.Examples { + example.Validate(r.Name) + } + + if r.Async != nil { + r.Async.Validate() } } diff --git a/mmv1/api/resource/custom_code.go b/mmv1/api/resource/custom_code.go index 48a69aa40e26..a00d09ce5467 100644 --- a/mmv1/api/resource/custom_code.go +++ b/mmv1/api/resource/custom_code.go @@ -136,24 +136,3 @@ type CustomCode struct { // with a success HTTP code for deleted resources TestCheckDestroy string `yaml:"test_check_destroy"` } - -// def validate -// super - -// check :extra_schema_entry, type: String -// check :encoder, type: String -// check :update_encoder, type: String -// check :decoder, type: String -// check :constants, type: String -// check :pre_create, type: String -// check :post_create, type: String -// check :custom_create, type: String -// check :pre_read, type: String -// check :pre_update, type: String -// check :post_update, type: String -// check :custom_update, type: String -// check :pre_delete, type: String -// check :custom_import, type: String -// check :post_import, type: String -// check :test_check_destroy, type: String -// end diff --git a/mmv1/api/resource/docs.go b/mmv1/api/resource/docs.go index 2b81541c09c3..2de8004d2350 100644 --- a/mmv1/api/resource/docs.go +++ b/mmv1/api/resource/docs.go @@ -41,12 +41,3 @@ type Docs struct { // attr_reader : Attributes string } - -// def validate -// super -// check :warning, type: String -// check :note, type: String -// check :required_properties, type: String -// check :optional_properties, type: String -// check :attributes, type: String -// end diff --git a/mmv1/api/resource/examples.go b/mmv1/api/resource/examples.go index bc24fede5793..8d618b7b94fb 100644 --- a/mmv1/api/resource/examples.go +++ b/mmv1/api/resource/examples.go @@ -16,15 +16,16 @@ package resource import ( "bytes" "fmt" + "log" "net/url" "path/filepath" "regexp" + "slices" "strings" "text/template" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "github.com/golang/glog" - "gopkg.in/yaml.v3" ) // Generates configs to be shown as examples in docs and outputted as tests @@ -163,11 +164,11 @@ type Examples struct { } // Set default value for fields -func (e *Examples) UnmarshalYAML(n *yaml.Node) error { +func (e *Examples) UnmarshalYAML(unmarshal func(any) error) error { type exampleAlias Examples aliasObj := (*exampleAlias)(e) - err := n.Decode(&aliasObj) + err := unmarshal(aliasObj) if err != nil { return err } @@ -180,6 +181,33 @@ func (e *Examples) UnmarshalYAML(n *yaml.Node) error { return nil } +func (e *Examples) Validate(rName string) { + if e.Name == "" { + log.Fatalf("Missing `name` for one example in resource %s", rName) + } + e.ValidateExternalProviders() +} + +func (e *Examples) ValidateExternalProviders() { + // Official providers supported by HashiCorp + // https://registry.terraform.io/search/providers?namespace=hashicorp&tier=official + HASHICORP_PROVIDERS := []string{"aws", "random", "null", "template", "azurerm", "kubernetes", "local", + "external", "time", "vault", "archive", "tls", "helm", "azuread", "http", "cloudinit", "tfe", "dns", + "consul", "vsphere", "nomad", "awscc", "googleworkspace", "hcp", "boundary", "ad", "azurestack", "opc", + "oraclepaas", "hcs", "salesforce"} + + var unallowedProviders []string + for _, p := range e.ExternalProviders { + if !slices.Contains(HASHICORP_PROVIDERS, p) { + unallowedProviders = append(unallowedProviders, p) + } + } + + if len(unallowedProviders) > 0 { + log.Fatalf("Providers %#v are not allowed. Only providers published by HashiCorp are allowed.", unallowedProviders) + } +} + // Executes example templates for documentation and tests func (e *Examples) SetHCLText() { originalVars := e.Vars @@ -357,9 +385,6 @@ func SubstituteTestPaths(config string) string { // check :skip_vcr, type: TrueClass // } -// TODO -// validate_external_providers - // func (e *Examples) merge(other) { // result = self.class.new // instance_variables.each do |v| diff --git a/mmv1/api/resource/iam_policy.go b/mmv1/api/resource/iam_policy.go index 812e18ab8170..18708e48fd57 100644 --- a/mmv1/api/resource/iam_policy.go +++ b/mmv1/api/resource/iam_policy.go @@ -14,7 +14,8 @@ package resource import ( - "gopkg.in/yaml.v3" + "log" + "slices" ) // Information about the IAM policy for this resource @@ -117,7 +118,7 @@ type IamPolicy struct { SubstituteZoneValue bool `yaml:"substitute_zone_value"` } -func (p *IamPolicy) UnmarshalYAML(n *yaml.Node) error { +func (p *IamPolicy) UnmarshalYAML(unmarshal func(any) error) error { p.MethodNameSeparator = "/" p.FetchIamPolicyVerb = "GET" p.FetchIamPolicyMethod = "getIamPolicy" @@ -132,7 +133,7 @@ func (p *IamPolicy) UnmarshalYAML(n *yaml.Node) error { type iamPolicyAlias IamPolicy aliasObj := (*iamPolicyAlias)(p) - err := n.Decode(&aliasObj) + err := unmarshal(aliasObj) if err != nil { return err } @@ -140,6 +141,19 @@ func (p *IamPolicy) UnmarshalYAML(n *yaml.Node) error { return nil } -// func (p *IamPolicy) validate() { +func (p *IamPolicy) Validate(rName string) { + allowed := []string{"GET", "POST"} + if !slices.Contains(allowed, p.FetchIamPolicyVerb) { + log.Fatalf("Value on `fetch_iam_policy_verb` should be one of %#v in resource %s", allowed, rName) + } + + allowed = []string{"POST", "PUT"} + if !slices.Contains(allowed, p.SetIamPolicyVerb) { + log.Fatalf("Value on `set_iam_policy_verb` should be one of %#v in resource %s", allowed, rName) + } -// } + allowed = []string{"REQUEST_BODY", "QUERY_PARAM", "QUERY_PARAM_NESTED"} + if p.IamConditionsRequestType != "" && !slices.Contains(allowed, p.IamConditionsRequestType) { + log.Fatalf("Value on `iam_conditions_request_type` should be one of %#v in resource %s", allowed, rName) + } +} diff --git a/mmv1/api/resource/nested_query.go b/mmv1/api/resource/nested_query.go index 174f46f8d367..a0ebea0198e1 100644 --- a/mmv1/api/resource/nested_query.go +++ b/mmv1/api/resource/nested_query.go @@ -13,6 +13,8 @@ package resource +import "log" + // Metadata for resources that are nested within a parent resource, as // a list of resources or single object within the parent. // e.g. Fine-grained resources @@ -43,10 +45,8 @@ type NestedQuery struct { ModifyByPatch bool `yaml:"modify_by_patch"` } -// def validate -// super - -// check :keys, type: Array, item_type: String, required: true -// check :is_list_of_ids, type: :boolean, default: false -// check :modify_by_patch, type: :boolean, default: false -// end +func (q *NestedQuery) Validate(rName string) { + if len(q.Keys) == 0 { + log.Fatalf("Missing `keys` for `nested_query` in resource %s", rName) + } +} diff --git a/mmv1/api/resource/reference_links.go b/mmv1/api/resource/reference_links.go index 6237308ffb4d..5c4862bbaae8 100644 --- a/mmv1/api/resource/reference_links.go +++ b/mmv1/api/resource/reference_links.go @@ -13,14 +13,8 @@ package resource -import ( - "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" -) - // Represents a list of documentation links. type ReferenceLinks struct { - google.YamlValidator - // guides containing // name: The title of the link // value: The URL to navigate on click @@ -33,7 +27,3 @@ type ReferenceLinks struct { //attr_reader Api string } - -// func (l *ReferenceLinks) validate() { - -// } diff --git a/mmv1/api/resource/sweeper.go b/mmv1/api/resource/sweeper.go index 7ba3e789e203..ebc078c5a770 100644 --- a/mmv1/api/resource/sweeper.go +++ b/mmv1/api/resource/sweeper.go @@ -19,9 +19,3 @@ type Sweeper struct { // eligibility for deletion for generated resources SweepableIdentifierField string `yaml:"sweepable_identifier_field"` } - -// def validate -// super - -// check :sweepable_identifier_field, type: String -// end diff --git a/mmv1/api/resource/validation.go b/mmv1/api/resource/validation.go index a49ba30a7ddb..24cbfb557758 100644 --- a/mmv1/api/resource/validation.go +++ b/mmv1/api/resource/validation.go @@ -20,10 +20,3 @@ type Validation struct { Regex string Function string } - -// def validate -// super - -// check :regex, type: String -// check :function, type: String -// end diff --git a/mmv1/api/timeouts.go b/mmv1/api/timeouts.go index 5e8c77ac76e8..93380777e660 100644 --- a/mmv1/api/timeouts.go +++ b/mmv1/api/timeouts.go @@ -40,11 +40,3 @@ func NewTimeouts() *Timeouts { DeleteMinutes: DEFAULT_DELETE_TIMEOUT_MINUTES, } } - -// def validate -// super - -// check :insert_minutes, type: Integer, default: DEFAULT_INSERT_TIMEOUT_MINUTES -// check :update_minutes, type: Integer, default: DEFAULT_UPDATE_TIMEOUT_MINUTES -// check :delete_minutes, type: Integer, default: DEFAULT_DELETE_TIMEOUT_MINUTES -// end diff --git a/mmv1/api/type.go b/mmv1/api/type.go index b83028e81b5f..df6cbfb18bf7 100644 --- a/mmv1/api/type.go +++ b/mmv1/api/type.go @@ -337,6 +337,28 @@ func (t *Type) SetDefault(r *Resource) { } } +func (t *Type) Validate(rName string) { + if t.Output && t.Required { + log.Fatalf("Property %s cannot be output and required at the same time in resource %s.", t.Name, rName) + } + + if t.DefaultFromApi && t.DefaultValue != nil { + log.Fatalf("'default_value' and 'default_from_api' cannot be both set in resource %s", rName) + } + + switch { + case t.IsA("Array"): + t.ItemType.Validate(rName) + case t.IsA("Map"): + t.ValueType.Validate(rName) + case t.IsA("NestedObject"): + for _, p := range t.Properties { + p.Validate(rName) + } + default: + } +} + // super // check :description, type: ::String, required: true // check :exclude, type: :boolean, default: false, required: true diff --git a/mmv1/go.mod b/mmv1/go.mod index 3312c96d0e5b..6ba2aa672c36 100644 --- a/mmv1/go.mod +++ b/mmv1/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 - gopkg.in/yaml.v3 v3.0.1 + gopkg.in/yaml.v2 v2.4.0 ) require github.com/golang/glog v1.2.0 diff --git a/mmv1/go.sum b/mmv1/go.sum index 9e56cc0f1cb1..02e4ed2c5647 100644 --- a/mmv1/go.sum +++ b/mmv1/go.sum @@ -6,5 +6,5 @@ golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUF golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/mmv1/google/yaml_validator.go b/mmv1/google/yaml_validator.go index 485db94d1dd0..811246cea98e 100644 --- a/mmv1/google/yaml_validator.go +++ b/mmv1/google/yaml_validator.go @@ -16,7 +16,7 @@ package google import ( "log" - "gopkg.in/yaml.v3" + "gopkg.in/yaml.v2" ) // A helper class to validate contents coming from YAML files. @@ -26,7 +26,7 @@ func (v *YamlValidator) Parse(content []byte, obj interface{}, yamlPath string) // TODO(nelsonjr): Allow specifying which symbols to restrict it further. // But it requires inspecting all configuration files for symbol sources, // such as Enum values. Leaving it as a nice-to-have for the future. - if err := yaml.Unmarshal(content, obj); err != nil { + if err := yaml.UnmarshalStrict(content, obj); err != nil { log.Fatalf("Cannot unmarshal data from file %s: %v", yamlPath, err) } }