Skip to content

Commit

Permalink
[CMSP-27] Implement sites.yml parsing (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
pwtyler authored Feb 8, 2023
1 parent 630a236 commit 63c839c
Show file tree
Hide file tree
Showing 17 changed files with 614 additions and 1 deletion.
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

0 comments on commit 63c839c

Please sign in to comment.