Skip to content
This repository was archived by the owner on Jan 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions internal/cli/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,29 @@ func (c *baseCommand) Init(opts ...Option) error {
// Try parsing config
c.cfg, err = c.initConfig("")
if err != nil {
c.logError(c.Log, "failed to load config", err)
return err
var shouldExit bool

if validationResults, ok := err.(config.ValidationResults); ok {
c.ui.Output("The following validation issues were detected:", terminal.WithHeaderStyle())

for _, vr := range validationResults {
if vr.Error != nil {
shouldExit = true
c.ui.Output(vr.Error.Error(), terminal.WithErrorStyle())
} else if vr.Warning != "" {
c.ui.Output(vr.Warning, terminal.WithWarningStyle())
}
}

if shouldExit {
return err
}

c.ui.Output("")
} else {
c.logError(c.Log, "failed to load config", err)
return err
}
}

// If that worked, set our refs
Expand Down
143 changes: 91 additions & 52 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"regexp"
"strings"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"
Expand Down Expand Up @@ -48,6 +47,33 @@ type validateVariable struct {
Description string `hcl:"description,optional"`
}

type ValidationResult struct {
Error error
Warning string
}

func (v ValidationResult) String() string {
if v.Error != nil {
return v.Error.Error()
}

return "warning: " + v.Warning
}

type ValidationResults []ValidationResult

func (v ValidationResults) Error() string {
var values []string

for _, res := range v {
values = append(values, res.String())
}

return fmt.Sprintf("%d validation errors: %s", len(v), strings.Join(values, ", "))
}

const AppeningHappening = "More than one app stanza within a waypoint.hcl file is deprecated, and will be removed in 0.10.\nPlease see https://discuss.hashicorp.com/waypoint/xxyyzz for more information."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👨🏻‍🍳💋


// Validate the structure of the configuration.
//
// This will validate required fields are specified and the types of some fields.
Expand All @@ -56,148 +82,160 @@ type validateVariable struct {
//
// Users of this package should call Validate on each subsequent configuration
// that is loaded (Apps, Builds, Deploys, etc.) for further rich validation.
func (c *Config) Validate() error {
func (c *Config) Validate() ValidationResults {
var results ValidationResults

// Validate root
schema, _ := gohcl.ImpliedBodySchema(&validateStruct{})
content, diag := c.hclConfig.Body.Content(schema)
if diag.HasErrors() {
return diag
}
results = append(results, ValidationResult{Error: diag})

var result error
if content == nil {
return results
}
}

// Require the project. We don't use an "attr" above (which would require it)
// because the project can be populated later such as in a runner which
// sets it to the project in the job ref.
if c.Project == "" {
result = multierror.Append(result, fmt.Errorf("'project' attribute is required"))
results = append(results, ValidationResult{Error: fmt.Errorf("'project' attribute is required")})
}

apps := content.Blocks.OfType("app")

// Validate apps
for _, block := range content.Blocks.OfType("app") {
err := c.validateApp(block)
if err != nil {
result = multierror.Append(result, err)
}
for _, block := range apps {
appRes := c.validateApp(block)
results = append(results, appRes...)
}

// Validate labels
if errs := ValidateLabels(c.Labels); len(errs) > 0 {
result = multierror.Append(result, errs...)
if len(apps) > 1 {
results = append(results, ValidationResult{Warning: AppeningHappening})
}

return result
// Validate labels
labelResults := ValidateLabels(c.Labels)
results = append(results, labelResults...)

return results
}

func (c *Config) validateApp(b *hcl.Block) error {
func (c *Config) validateApp(b *hcl.Block) []ValidationResult {
var results []ValidationResult

// Validate root
schema, _ := gohcl.ImpliedBodySchema(&validateApp{})
content, diag := b.Body.Content(schema)
if diag.HasErrors() {
return diag
results = append(results, ValidationResult{Error: diag})

if content == nil {
return results
}
}

// Build required
if len(content.Blocks.OfType("build")) != 1 {
return &hcl.Diagnostic{
results = append(results, ValidationResult{Error: &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "'build' stanza required",
Subject: &b.DefRange,
Context: &b.TypeRange,
}
}})
}

// Deploy required
if len(content.Blocks.OfType("deploy")) != 1 {
return &hcl.Diagnostic{
results = append(results, ValidationResult{Error: &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "'deploy' stanza required",
Subject: &b.DefRange,
Context: &b.TypeRange,
}
}})
}

return nil
return results
}

// Validate validates the application.
//
// Similar to Config.App, this doesn't validate configuration that is
// further deferred such as build, deploy, etc. stanzas so call Validate
// on those as they're loaded.
func (c *App) Validate() error {
var result error
func (c *App) Validate() ValidationResults {
var results ValidationResults

// Validate labels
if errs := ValidateLabels(c.Labels); len(errs) > 0 {
result = multierror.Append(result, errs...)
}
labelResults := ValidateLabels(c.Labels)
results = append(results, labelResults...)

// If a path is specified, it must not be a child of the root.
if c.Path != "" {
if !filepath.IsAbs(c.Path) {
// This should never happen because during App load time
// we ensure that the path is absolute relative to the project
// path.
panic("path is not absolute")
results = append(results, ValidationResult{Error: fmt.Errorf("path is not absolute")})
}

rel, err := filepath.Rel(c.config.path, c.Path)
if err != nil {
result = multierror.Append(result, fmt.Errorf(
"path: must be a child of the project directory"))
results = append(results, ValidationResult{Error: fmt.Errorf(
"path: must be a child of the project directory")})
}
if strings.HasPrefix(rel, "../") || strings.HasPrefix(rel, "..\\") {
result = multierror.Append(result, fmt.Errorf(
"path: must be a child of the project directory"))
results = append(results, ValidationResult{Error: fmt.Errorf(
"path: must be a child of the project directory")})
}
}

if c.BuildRaw == nil || c.BuildRaw.Use == nil || c.BuildRaw.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
"build stage with a default 'use' stanza is required"))
results = append(results, ValidationResult{Error: fmt.Errorf(
"build stage with a default 'use' stanza is required")})
}

if c.DeployRaw == nil || c.DeployRaw.Use == nil || c.DeployRaw.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
"deploy stage with a default 'use' stanza is required"))
results = append(results, ValidationResult{Error: fmt.Errorf(
"deploy stage with a default 'use' stanza is required")})
}

for _, scope := range c.BuildRaw.WorkspaceScoped {
if scope.Use == nil || scope.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
results = append(results, ValidationResult{Error: fmt.Errorf(
"build: workspace scope %q: 'use' stanza is required",
scope.Scope,
))
)})
}
}
for _, scope := range c.BuildRaw.LabelScoped {
if scope.Use == nil || scope.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
results = append(results, ValidationResult{Error: fmt.Errorf(
"build: label scope %q: 'use' stanza is required",
scope.Scope,
))
)})
}
}

for _, scope := range c.DeployRaw.WorkspaceScoped {
if scope.Use == nil || scope.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
results = append(results, ValidationResult{Error: fmt.Errorf(
"deploy: workspace scope %q: 'use' stanza is required",
scope.Scope,
))
)})
}
}
for _, scope := range c.DeployRaw.LabelScoped {
if scope.Use == nil || scope.Use.Type == "" {
result = multierror.Append(result, fmt.Errorf(
results = append(results, ValidationResult{Error: fmt.Errorf(
"deploy: label scope %q: 'use' stanza is required",
scope.Scope,
))
)})
}
}

return result
return results
}

// ValidateLabels validates a set of labels. This ensures that labels are
Expand All @@ -207,29 +245,30 @@ func (c *App) Validate() error {
// * keys must be in hostname format (RFC 952)
// * keys can't be prefixed with "waypoint/" which is reserved for system use
//
func ValidateLabels(labels map[string]string) []error {
var errs []error
func ValidateLabels(labels map[string]string) ValidationResults {
var results ValidationResults

for k, v := range labels {
name := fmt.Sprintf("label[%s]", k)

if strings.HasPrefix(k, "waypoint/") {
errs = append(errs, fmt.Errorf("%s: prefix 'waypoint/' is reserved for system use", name))
results = append(results, ValidationResult{Error: fmt.Errorf("%s: prefix 'waypoint/' is reserved for system use", name)})
}

if len(k) > 255 {
errs = append(errs, fmt.Errorf("%s: key must be less than or equal to 255 characters", name))
results = append(results, ValidationResult{Error: fmt.Errorf("%s: key must be less than or equal to 255 characters", name)})
}

if !hostnameRegexRFC952.MatchString(strings.SplitN(k, "/", 2)[0]) {
errs = append(errs, fmt.Errorf("%s: key before '/' must be a valid hostname (RFC 952)", name))
results = append(results, ValidationResult{Error: fmt.Errorf("%s: key before '/' must be a valid hostname (RFC 952)", name)})
}

if len(v) > 255 {
errs = append(errs, fmt.Errorf("%s: value must be less than or equal to 255 characters", name))
results = append(results, ValidationResult{Error: fmt.Errorf("%s: value must be less than or equal to 255 characters", name)})
}
}

return errs
return results
}

var hostnameRegexRFC952 = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9\-]+[\.]?)*[a-zA-Z0-9]$`)
5 changes: 2 additions & 3 deletions internal/core/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

Expand Down Expand Up @@ -110,8 +109,8 @@ func NewProject(ctx context.Context, os ...Option) (*Project, error) {
if err := opts.Config.Validate(); err != nil {
return nil, err
}
if errs := config.ValidateLabels(p.overrideLabels); len(errs) > 0 {
return nil, multierror.Append(nil, errs...)
if err := config.ValidateLabels(p.overrideLabels); err != nil {
return nil, err
}

// Init our server connection. This may be in-process if we're in
Expand Down