From 7076a8d661ee3a62b68fb8af5f4640a8ece0e24c Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Tue, 9 Apr 2024 12:51:51 -0400 Subject: [PATCH] api: Add custom "required_for_put" tag --- internal/api/arm/error.go | 2 + internal/api/hcpopenshiftcluster.go | 32 +++++++-------- internal/api/registry.go | 39 +++++++++++++++++++ .../hcpopenshiftclusters_methods.go | 2 +- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/internal/api/arm/error.go b/internal/api/arm/error.go index 95d0145d6..59fff2264 100644 --- a/internal/api/arm/error.go +++ b/internal/api/arm/error.go @@ -137,6 +137,8 @@ func WriteUnmarshalError(err error, w http.ResponseWriter) { message += fmt.Sprintf(" (must be one of: %s)", fieldErr.Param()) } else { switch tag { + case "required_for_put": // custom tag + message = fmt.Sprintf("Missing required field '%s'", fieldErr.Field()) case "cidrv4": message += " (must be a v4 CIDR address)" case "ipv4": diff --git a/internal/api/hcpopenshiftcluster.go b/internal/api/hcpopenshiftcluster.go index f3936e58d..161d485c1 100644 --- a/internal/api/hcpopenshiftcluster.go +++ b/internal/api/hcpopenshiftcluster.go @@ -12,27 +12,27 @@ import ( // HCPOpenShiftCluster represents an ARO HCP OpenShift cluster resource. type HCPOpenShiftCluster struct { arm.TrackedResource - Properties HCPOpenShiftClusterProperties `json:"properties,omitempty"` + Properties HCPOpenShiftClusterProperties `json:"properties,omitempty" validate:"required_for_put"` } // HCPOpenShiftClusterProperties represents the property bag of a HCPOpenShiftCluster resource. type HCPOpenShiftClusterProperties struct { ProvisioningState arm.ProvisioningState `json:"provisioningState,omitempty" visibility:"read" validate:"omitempty,enum_provisioningstate"` - Spec ClusterSpec `json:"spec,omitempty" visibility:"read,create,update"` + Spec ClusterSpec `json:"spec,omitempty" visibility:"read,create,update" validate:"required_for_put"` } // ClusterSpec represents a high level cluster configuration. type ClusterSpec struct { - Version VersionProfile `json:"version,omitempty" visibility:"read,create,update"` + Version VersionProfile `json:"version,omitempty" visibility:"read,create,update" validate:"required_for_put"` DNS DNSProfile `json:"dns,omitempty" visibility:"read,create,update"` Network NetworkProfile `json:"network,omitempty" visibility:"read,create"` Console ConsoleProfile `json:"console,omitempty" visibility:"read"` - API APIProfile `json:"api,omitempty" visibility:"read,create"` + API APIProfile `json:"api,omitempty" visibility:"read,create" validate:"required_for_put"` FIPS bool `json:"fips,omitempty" visibility:"read,create"` EtcdEncryption bool `json:"etcdEncryption,omitempty" visibility:"read,create"` DisableUserWorkloadMonitoring bool `json:"disableUserWorkloadMonitoring,omitempty" visibility:"read,create,update"` Proxy ProxyProfile `json:"proxy,omitempty" visibility:"read,create,update"` - Platform PlatformProfile `json:"platform,omitempty" visibility:"read,create"` + Platform PlatformProfile `json:"platform,omitempty" visibility:"read,create" validate:"required_for_put"` IssuerURL string `json:"issuerUrl,omitempty" visibility:"read" validate:"omitempty,url"` ExternalAuth ExternalAuthConfigProfile `json:"externalAuth,omitempty" visibility:"read,create"` Ingress []*IngressProfile `json:"ingressProfile,omitempty" visibility:"read,create"` @@ -40,24 +40,24 @@ type ClusterSpec struct { // VersionProfile represents the cluster control plane version. type VersionProfile struct { - ID string `json:"id,omitempty" visibility:"read,create,update"` - ChannelGroup string `json:"channelGroup,omitempty" visibility:"read,create"` + ID string `json:"id,omitempty" visibility:"read,create,update" validate:"required_for_put"` + ChannelGroup string `json:"channelGroup,omitempty" visibility:"read,create" validate:"required_for_put"` AvailableUpgrades []string `json:"availableUpgrades,omitempty" visibility:"read"` } // DNSProfile represents the DNS configuration of the cluster. type DNSProfile struct { BaseDomain string `json:"baseDomain,omitempty" visibility:"read"` - BaseDomainPrefix string `json:"baseDomainPrefix,omitempty" visibility:"read,create"` + BaseDomainPrefix string `json:"baseDomainPrefix,omitempty" visibility:"read,create" validate:"required_for_put"` } // NetworkProfile represents a cluster network configuration. // Visibility for the entire struct is "read,create". type NetworkProfile struct { NetworkType NetworkType `json:"networkType,omitempty"` - PodCIDR string `json:"podCidr,omitempty" validate:"omitempty,cidrv4"` - ServiceCIDR string `json:"serviceCidr,omitempty" validate:"omitempty,cidrv4"` - MachineCIDR string `json:"machineCidr,omitempty" validate:"omitempty,cidrv4"` + PodCIDR string `json:"podCidr,omitempty" validate:"required_for_put,cidrv4"` + ServiceCIDR string `json:"serviceCidr,omitempty" validate:"required_for_put,cidrv4"` + MachineCIDR string `json:"machineCidr,omitempty" validate:"required_for_put,cidrv4"` HostPrefix int32 `json:"hostPrefix,omitempty"` } @@ -71,7 +71,7 @@ type ConsoleProfile struct { type APIProfile struct { URL string `json:"url,omitempty" visibility:"read" validate:"omitempty,url"` IP string `json:"ip,omitempty" visibility:"read" validate:"omitempty,ipv4"` - Visibility Visibility `json:"visibility,omitempty" visibility:"read,create" validate:"omitempty,enum_visibility"` + Visibility Visibility `json:"visibility,omitempty" visibility:"read,create" validate:"required_for_put,enum_visibility"` } // ProxyProfile represents the cluster proxy configuration. @@ -86,10 +86,10 @@ type ProxyProfile struct { // PlatformProfile represents the Azure platform configuration. // Visibility for the entire struct is "read,create". type PlatformProfile struct { - ManagedResourceGroup string `json:"managedResourceGroup,omitempty"` - SubnetID string `json:"subnetId,omitempty"` + ManagedResourceGroup string `json:"managedResourceGroup,omitempty" validate:"required_for_put"` + SubnetID string `json:"subnetId,omitempty" validate:"required_for_put"` OutboundType OutboundType `json:"outboundType,omitempty" validate:"omitempty,enum_outboundtype"` - PreconfiguredNSGs bool `json:"preconfiguredNsgs,omitempty"` + PreconfiguredNSGs bool `json:"preconfiguredNsgs,omitempty" validate:"required_for_put"` EtcdEncryptionSetID string `json:"etcdEncryptionSetId,omitempty"` } @@ -103,7 +103,7 @@ type ExternalAuthConfigProfile struct { type IngressProfile struct { IP string `json:"ip,omitempty" visibility:"read" validate:"omitempty,ipv4"` URL string `json:"url,omitempty" visibility:"read" validate:"omitempty,url"` - Visibility Visibility `json:"visibility,omitempty" visibility:"read,create" validate:"omitempty,enum_visibility"` + Visibility Visibility `json:"visibility,omitempty" visibility:"read,create" validate:"required_for_put,enum_visibility"` } // Creates an HCPOpenShiftCluster with any non-zero default values. diff --git a/internal/api/registry.go b/internal/api/registry.go index d9513529d..010670e19 100644 --- a/internal/api/registry.go +++ b/internal/api/registry.go @@ -5,6 +5,7 @@ package api import ( "fmt" + "net/http" "reflect" "strings" @@ -65,5 +66,43 @@ func NewValidator() *validator.Validate { return name }) + // Use this for fields required in PUT requests. Do not apply to read-only fields. + validate.RegisterValidation("required_for_put", func(fl validator.FieldLevel) bool { + val := fl.Top().FieldByName("Method") + if val.IsZero() { + panic("Method field not found for required_for_put") + } + if val.String() != http.MethodPut { + return true + } + + // This is replicating the implementation of "required". + // See https://github.com/go-playground/validator/issues/492 + // Sounds like "hasValue" is unlikely to be exported and + // "validate.Var" does not seem like a safe alternative. + field := fl.Field() + _, kind, nullable := fl.ExtractType(field) + switch kind { + case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func: + return !field.IsNil() + default: + if nullable && field.Interface() != nil { + return true + } + return field.IsValid() && !field.IsZero() + } + }) + return validate } + +type validateContext struct { + // Fields must be exported so valdator can access. + Method string + Updating bool + Resource any +} + +func ValidateRequest(validate *validator.Validate, method string, updating bool, resource any) error { + return validate.Struct(validateContext{Method: method, Updating: updating, Resource: resource}) +} diff --git a/internal/api/v20240610preview/hcpopenshiftclusters_methods.go b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go index 9fec22f83..4f326449e 100644 --- a/internal/api/v20240610preview/hcpopenshiftclusters_methods.go +++ b/internal/api/v20240610preview/hcpopenshiftclusters_methods.go @@ -213,7 +213,7 @@ func (v version) UnmarshalHCPOpenShiftCluster(data []byte, out *api.HCPOpenShift resource.Normalize(out) - return validate.Struct(out) + return api.ValidateRequest(validate, method, updating, out) } func (c *HcpOpenShiftClusterResource) Normalize(out *api.HCPOpenShiftCluster) {