Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CMSP-27] Implement sites.yml parsing #1

Merged
merged 25 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
.DS_Store

dist/

*.out
27 changes: 27 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1 +1,28 @@
# Sites.yml | Pantheon.yml Validator

A utility for validating a sites.yml file on a pantheon site during WordPress multisites' search-replace tasks. Asprirationally to include pantheon.yml validation in the future.

# Usage

## Sites.yml
```
$ pyml-validator sites -f path/to/sites.yml
```

See [this annotated fixture](./fixtures/sites/valid.yml) for an example of a valid sites.yml file.

## Pantheon.yml
Note, validation of pantheon.yml is unimplemented, so any file reads as valid.
```
$ pyml-validator pantheon -f path/to/pantheon.yml
```

# Testing

[![Coverage Status](https://coveralls.io/repos/github/pantheon-systems/pyml-validator/badge.svg?t=PGhafd)](https://coveralls.io/github/pantheon-systems/pyml-validator)

`make test` runs linting and testing.

# Releases

Automatically releases on merge to main via autotag + goreleaser. See [Autotag Readme](https://github.com/pantheon-systems/autotag) for details on how the SemVer is determined. Note, with goreleaser, each commit merged will become a line item in the release's Changelog. Take note to use squashing and/or rebase to ensure helpful and informative commit messages.
30 changes: 30 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"log"

"github.com/spf13/cobra"
)

var FilePath string

var rootCmd = &cobra.Command{
Use: "pyml-validator",
Short: "Pyml-validator validates pantheon.yml, sites.yml, etc.",
Long: `Pyml-validator is a validator for pantheon.yml or sites.yml.
Ensures that the given config file can be used by the platform.`,
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

func init() {
rootCmd.PersistentFlags().StringVarP(&FilePath, "file", "f", "", "path/to/file.yml")
err := rootCmd.MarkPersistentFlagRequired("file")
if err != nil {
log.Fatal(err)
}
}
49 changes: 49 additions & 0 deletions cmd/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"fmt"
"pyml-validator/pkg/validator"

"github.com/spf13/cobra"
)

func validatorCommand(cmd *cobra.Command) error {
// Is there a better way to do this? Without this we print usage on error exits.
// If we override at the root level, we don't get usage when we _do_ want it.
cmd.SilenceUsage = true

v, err := validator.ValidatorFactory(cmd.Use)
if err != nil {
return err
}

err = v.ValidateFromFilePath(FilePath)
if err != nil {
return err
}
fmt.Printf("✨ %s.yml is valid\n", cmd.Use)
return nil
}

var sitesCommand = &cobra.Command{
Use: "sites",
Short: "validate sites.yml",
Long: `Validate sites.yml`,
RunE: func(cmd *cobra.Command, args []string) error {
return validatorCommand(cmd)
},
}

var pantheonCommand = &cobra.Command{
Use: "pantheon",
Short: "validate pantheon.yml",
Long: `Validate pantheon.yml. For more information, see https://pantheon.io/docs/pantheon-yml`,
RunE: func(cmd *cobra.Command, args []string) error {
return validatorCommand(cmd)
},
}

func init() {
rootCmd.AddCommand(pantheonCommand)
rootCmd.AddCommand(sitesCommand)
}
2 changes: 2 additions & 0 deletions fixtures/sites/invalid_api_version_only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
api_version: 2
31 changes: 31 additions & 0 deletions fixtures/sites/valid.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
api_version: 1 # Currently only one api version.

# "domain_maps" is a collection of blog URLs for each environment used to
# facilitate search-replace of a WordPress Multisite (WPMS) across pantheon
# environments. Each key of "domain_maps" must be a valid environment name.
domain_maps:
# environment: <collection of domains to be used on this environment>
# i.e. dev, test, live, feat-branch, &c.
dev:
# each environment collection maps the blog ID to its URL. A url must be
# set in both the target and source environments for search-replace to be
# run.
# i.e. 1: blog1-mysite.com
1: about.dev-mysite.pantheonsite.io
2: employee-resources.dev-mysite.pantheonsite.io
3: staff-portal.dev-mysite.pantheonsite.io
test:
1: about.test-mysite.pantheonsite.io
2: employee-resources.test-mysite.pantheonsite.io
3: staff-portal.test-mysite.pantheonsite.io
live:
1: about.mysite.com
2: employee-resources.mysite.com
3: staff-portal.mysite.com
autopilot:
1: about.autopilot-mysite.pantheonsite.io
2: employee-resources.autopilot-mysite.pantheonsite.io
3: staff-portal.autopilot-mysite.pantheonsite.io

# Anything else in the file will be ignored, but not rejected.
2 changes: 2 additions & 0 deletions fixtures/sites/valid_api_version_only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
api_version: 1
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
module pyml-validator

go 1.19

require (
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
25 changes: 25 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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=
6 changes: 5 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
package main

func main() {}
import "pyml-validator/cmd"

func main() {
cmd.Execute()
}
12 changes: 12 additions & 0 deletions pkg/model/sites.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package model

// SitesYml is used to map domains across environments for search and replace with WPMS sites.
type SitesYml struct {
APIVersion int `yaml:"api_version"`
DomainMaps DomainMaps `yaml:"domain_maps"`
}

type DomainMaps map[string]DomainMapByEnvironment

// DomainMapByEnvironment is a map of site (blog) domains keyed by blog ID.
type DomainMapByEnvironment map[int]string
22 changes: 22 additions & 0 deletions pkg/validator/pantheon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package validator

import (
"fmt"
"os"
)

type PantheonValidator struct{}

// ValidateFromYaml asserts a given pantheon.yaml file is valid.
// As this has not been implemented, nothing is invalid.
func (v *PantheonValidator) ValidateFromYaml(y []byte) error {
return nil
}

func (v *PantheonValidator) ValidateFromFilePath(filePath string) error {
yFile, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("error reading YAML file: %w", err)
}
return v.ValidateFromYaml(yFile)
}
81 changes: 81 additions & 0 deletions pkg/validator/sites.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package validator

import (
"fmt"
"os"
"pyml-validator/pkg/model"
"regexp"

"gopkg.in/yaml.v3"
)

const (
MaxDomainMaps = 25 // This could be raised
)

var (
// See https://github.com/pantheon-systems/titan-mt/blob/master/yggdrasil/lib/pantheon_yml/pantheon_yml_v1_schema.py
ValidHostnameRegex = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`)
ValidMultidevNameRegex = regexp.MustCompile(`^[a-z0-9\-]{1,11}$`)
)

type SitesValidator struct{}

// ValidateFromYaml asserts a given sites.yaml file is valid.
func (v *SitesValidator) ValidateFromYaml(y []byte) error {
var s model.SitesYml

err := yaml.Unmarshal(y, &s)
if err != nil {
return err
}
return v.validate(s)
}

func (v *SitesValidator) ValidateFromFilePath(filePath string) error {
yFile, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("error reading YAML file: %w", err)
}
return v.ValidateFromYaml(yFile)
}

// validate asserts all aspects of sites.yml are valid.
func (v *SitesValidator) validate(sites model.SitesYml) error {
err := validateAPIVersion(sites.APIVersion)
if err != nil {
return err
}
return validateDomainMaps(sites.DomainMaps)
}

// validateDomainMaps ensures the domain maps provided in sites.yml are valid
// by asserting cloud development environments names are valid, there are not
// too many domain maps listed for any environment, and that the hostnames
// provided are valid Pantheon hostnames.
func validateDomainMaps(domainMaps map[string]model.DomainMapByEnvironment) error {
for env, domainMap := range domainMaps {
if !ValidMultidevNameRegex.MatchString(env) {
return fmt.Errorf("%q is not a valid environment name", env)
}
domainMapCount := len(domainMap)
if domainMapCount > MaxDomainMaps {
return fmt.Errorf("%q has too many domains listed (%d). Maximum is %d", env, domainMapCount, MaxDomainMaps)
}
for _, domain := range domainMap {
if !ValidHostnameRegex.MatchString(domain) {
return fmt.Errorf("%q is not a valid hostname", domain)
}
}
}
return nil
}

// validateAPIVersion asserts if sites.yml has a valid api version set. Once
// more than one version is valid, this will need to be more more robust.
func validateAPIVersion(apiVersion int) error {
if apiVersion != 1 {
return ErrInvalidAPIVersion
}
return nil
}
10 changes: 10 additions & 0 deletions pkg/validator/sites_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package validator

import "errors"

var (
ErrInvalidAPIVersion = errors.New("Invalid API Version. Must be '1'")
)

// TODO: More dynamic errors could be refactored here, but likely only worth
// pursuing once we are passing errors back to customers
Loading