cli_tools/common/utils/validation/validation_utils.go (112 lines of code) (raw):
// Copyright 2019 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
daisy "github.com/GoogleCloudPlatform/compute-daisy"
"github.com/go-playground/validator/v10"
)
const (
rfc1035LabelRegexpStr = "[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]"
imageNameStr = "[a-z]([-a-z0-9]{0,61}[a-z0-9])?"
diskSnapshotNameStr = "^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$"
// projectID: "The unique, user-assigned ID of the Project. It must be 6 to 30
// lowercase letters, digits, or hyphens. It must start with a letter.
// Trailing hyphens are prohibited."
// -- https://cloud.google.com/resource-manager/reference/rest/v1/projects
projectIDStr = "(google\\.com\\:)?[a-z][-a-z0-9]{4,28}[a-z0-9]" //
)
var (
rfc1035LabelRegexp = regexp.MustCompile(rfc1035LabelRegexpStr)
fqdnRegexp = regexp.MustCompile(fmt.Sprintf("^((%v)\\.)+(%v)$", rfc1035LabelRegexpStr, rfc1035LabelRegexpStr))
imageNameRegexp = regexp.MustCompile(fmt.Sprintf("^%v$", imageNameStr))
imageURIRegexp = regexp.MustCompile(fmt.Sprintf("^projects/(%v)/global/images/(%v)$", projectIDStr, imageNameStr))
diskSnapshotNameRegexp = regexp.MustCompile(diskSnapshotNameStr)
projectIDRegexp = regexp.MustCompile(fmt.Sprintf("^%v$", projectIDStr))
)
// ValidateStringFlagNotEmpty returns error with error message stating field must be provided if
// value is empty string. Returns nil otherwise.
func ValidateStringFlagNotEmpty(flagValue string, flagKey string) error {
if flagValue == "" {
return daisy.Errf(fmt.Sprintf("The flag -%v must be provided", flagKey))
}
return nil
}
// ValidateExactlyOneOfStringFlagNotEmpty returns error with error message stating one of fields must be provided if
// value is empty string. Returns nil otherwise.
func ValidateExactlyOneOfStringFlagNotEmpty(flagKeyValues map[string]string) error {
var notEmpty []string
for k, v := range flagKeyValues {
if v != "" {
notEmpty = append(notEmpty, k)
}
}
if len(notEmpty) != 1 {
return daisy.Errf(fmt.Sprintf("Exactly one of -%v flags should be provided", strings.Join(notEmpty, ",-")))
}
return nil
}
// ValidateFqdn validates fully qualified domain name
func ValidateFqdn(flagValue string, flagKey string) error {
flagValueLen := len(flagValue)
if flagValueLen < 1 || flagValueLen > 253 || !fqdnRegexp.MatchString(flagValue) {
return daisy.Errf(fmt.Sprintf("The flag `%v` must conform to RFC 1035 requirements for valid hostnames. "+
"To meet this requirement, the value must contain a series of labels and each label is concatenated with a dot. "+
"Each label can be 1-63 characters long where each character can be a letter, a digit or a dash (`-`). The "+
"entire sequence must not exceed 253 characters.", flagKey))
}
return nil
}
// ValidateRfc1035Label validates a single label per RFC 1035
func ValidateRfc1035Label(value string) error {
if len(value) > 63 || !rfc1035LabelRegexp.MatchString(value) {
return daisy.Errf(fmt.Sprintf("Value `%v` must conform to RFC 1035 requirements for valid labels.", value))
}
return nil
}
// ValidateImageName validates whether a string is a valid image name, as defined by
// <https://cloud.google.com/compute/docs/reference/rest/v1/images>.
func ValidateImageName(value string) error {
if !imageNameRegexp.MatchString(value) {
return daisy.Errf("Image name `%v` must conform to https://cloud.google.com/compute/docs/reference/rest/v1/images", value)
}
return nil
}
// ValidateImageURI validates whether a string is a valid image URI, as defined by
// <https://cloud.google.com/compute/docs/reference/rest/v1/images> and returns
// image name and project ID if valid.
func ValidateImageURI(value string) (project string, imageName string, err error) {
if !imageURIRegexp.MatchString(value) {
return imageName, project, daisy.Errf("Image URI `%v` must conform to https://cloud.google.com/compute/docs/reference/rest/v1/images", value)
}
match := imageURIRegexp.FindStringSubmatch(value)
return match[1], match[3], nil
}
// ValidateSnapshotName validates whether a string is a valid disk snapshot name, as defined by
// <https://cloud.google.com/compute/docs/reference/rest/v1/snapshots>.
func ValidateSnapshotName(value string) error {
if !diskSnapshotNameRegexp.MatchString(value) {
return daisy.Errf("Snapshot name `%v` must conform to https://cloud.google.com/compute/docs/reference/rest/v1/snapshots", value)
}
return nil
}
// ValidateProjectID validates whether a string is a valid projectID, as defined by
// <https://cloud.google.com/resource-manager/reference/rest/v1/projects>.
func ValidateProjectID(value string) error {
if !projectIDRegexp.MatchString(value) {
return daisy.Errf("projectID `%v` must conform to https://cloud.google.com/resource-manager/reference/rest/v1/projects", value)
}
return nil
}
// ValidateStruct performs struct field validation based on field tags.
//
// Use the syntax from <https://github.com/go-playground/validator>. In addition,
// the following is supported:
//
// New validators:
// gce_disk_image_name: Validates using `ValidateImageName`
//
// Field names:
// To customize the field name in the error message, include a tag named 'name'.
func ValidateStruct(s interface{}) error {
validate := validator.New()
// Register new validators.
if err := validate.RegisterValidation("gce_disk_image_name", func(fl validator.FieldLevel) bool {
return ValidateImageName(fl.Field().String()) == nil
}); err != nil {
panic(err)
}
// Allow the error message's field name to be customized via a `name` struct tag.
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if name, found := fld.Tag.Lookup("name"); found {
return name
}
return fld.Name
})
// Run validation.
err := validate.Struct(s)
if err == nil {
return nil
}
// If validation fails:
// 1. Surface the first error.
// 2. Create a new error message. This ensures sensitive information
// is not leaked to anonymous logs.
var verr validator.ValidationErrors
if errors.As(err, &verr) && len(verr) > 0 {
firstErr := verr[0]
switch firstErr.Tag() {
case "required":
return errors.New(firstErr.Field() + " has to be specified")
case "gce_disk_image_name":
return ValidateImageName(firstErr.Value().(string))
}
}
// Panic to ensure that CLI arguments are not leaked. To safely show an argument
// to a user, inject it into a string template using `daisy.Errf`.
panic(fmt.Sprintf("Customize error: %v", err))
}