diff --git a/internal/lagoonyml/lagoon.go b/internal/lagoonyml/lagoon.go index 2b4dfcf..a64de4a 100644 --- a/internal/lagoonyml/lagoon.go +++ b/internal/lagoonyml/lagoon.go @@ -29,5 +29,12 @@ type Environment struct { // Lagoon represents the .lagoon.yml file. type Lagoon struct { - Environments map[string]Environment `json:"environments"` + ProductionRoutes ProductionRoutes `json:"production_routes"` + Environments map[string]Environment `json:"environments"` +} + +// ProductionRoutes represents active/standby route configurations. +type ProductionRoutes struct { + Active Environment `json:"active"` + Standby Environment `json:"standby"` } diff --git a/internal/lagoonyml/lint.go b/internal/lagoonyml/lint.go index 4334d2d..5a54afc 100644 --- a/internal/lagoonyml/lint.go +++ b/internal/lagoonyml/lint.go @@ -16,14 +16,53 @@ type Linter func(*Lagoon) error // `.lagoon.yml` is valid. func Lint(path string, linters ...Linter) error { var l Lagoon + l.Environments = make(map[string]Environment) + rawYAML, err := os.ReadFile(path) if err != nil { return fmt.Errorf("couldn't read %v: %v", path, err) } - err = yaml.Unmarshal(rawYAML, &l) + + // unmarshal the raw yaml into a map[string]interface{} + var li map[string]interface{} + err = yaml.Unmarshal(rawYAML, &li) if err != nil { - return fmt.Errorf("couldn't unmarshal %v: %v", path, err) + return fmt.Errorf(".lagoon.yml configuration not valid for %v: %v", path, err) } + + // check each block for ability to be unmarshalled + for key, block := range li { + if b, ok := block.(map[string]interface{}); ok { + switch key { + case "environments": + for env, config := range b { + c, _ := yaml.Marshal(config) + var le Environment + err = yaml.Unmarshal(c, &le) + if err != nil { + fmt.Printf("Warning: .lagoon.yml configuration not valid for environment '%s': %v\n", env, err) + } + l.Environments[env] = le + } + case "production_routes": + for env, config := range b { + c, _ := yaml.Marshal(config) + var le Environment + err = yaml.Unmarshal(c, &le) + if err != nil { + fmt.Printf("Warning: .lagoon.yml configuration not valid for production_routes '%s': %v\n", env, err) + } + if env == "active" { + l.ProductionRoutes.Active = le + } else if env == "standby" { + l.ProductionRoutes.Standby = le + } + } + } + } + } + + // run the linter for _, linter := range linters { if err := linter(&l); err != nil { return &ErrLint{ diff --git a/internal/lagoonyml/lint_test.go b/internal/lagoonyml/lint_test.go index ba98808..44c761e 100644 --- a/internal/lagoonyml/lint_test.go +++ b/internal/lagoonyml/lint_test.go @@ -35,6 +35,10 @@ func TestLint(t *testing.T) { input: "testdata/invalid.1.lagoon.yml", valid: false, }, + "valid.broken.0.lagoon.yml": { + input: "testdata/valid.broken.0.lagoon.yml", + valid: true, + }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { diff --git a/internal/lagoonyml/routeannotation.go b/internal/lagoonyml/routeannotation.go index 44d39ae..ef15a55 100644 --- a/internal/lagoonyml/routeannotation.go +++ b/internal/lagoonyml/routeannotation.go @@ -78,6 +78,78 @@ func RouteAnnotation() Linter { } } } + for _, routeMap := range l.ProductionRoutes.Active.Routes { + for rName, lagoonRoutes := range routeMap { + for _, lagoonRoute := range lagoonRoutes { + for iName, ingress := range lagoonRoute.Ingresses { + // auth-snippet + if _, ok := ingress.Annotations[authSnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on active environment, route %s, ingress %s: %s", + authSnippet, rName, iName, + "this annotation is restricted") + } + // configuration-snippet + if annotation, ok := validate(ingress.Annotations, validSnippets, + configurationSnippet); !ok { + return fmt.Errorf( + "invalid %s annotation on active environment, route %s, ingress %s: %s", + configurationSnippet, rName, iName, annotation) + } + // modsecurity-snippet + if _, ok := ingress.Annotations[modsecuritySnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on active environment, route %s, ingress %s: %s", + modsecuritySnippet, rName, iName, + "this annotation is restricted") + } + // server-snippet + if annotation, ok := validate(ingress.Annotations, validSnippets, + serverSnippet); !ok { + return fmt.Errorf( + "invalid %s annotation on active environment, route %s, ingress %s: %s", + serverSnippet, rName, iName, annotation) + } + } + } + } + } + for _, routeMap := range l.ProductionRoutes.Standby.Routes { + for rName, lagoonRoutes := range routeMap { + for _, lagoonRoute := range lagoonRoutes { + for iName, ingress := range lagoonRoute.Ingresses { + // auth-snippet + if _, ok := ingress.Annotations[authSnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on standby environment, route %s, ingress %s: %s", + authSnippet, rName, iName, + "this annotation is restricted") + } + // configuration-snippet + if annotation, ok := validate(ingress.Annotations, validSnippets, + configurationSnippet); !ok { + return fmt.Errorf( + "invalid %s annotation on standby environment, route %s, ingress %s: %s", + configurationSnippet, rName, iName, annotation) + } + // modsecurity-snippet + if _, ok := ingress.Annotations[modsecuritySnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on standby environment, route %s, ingress %s: %s", + modsecuritySnippet, rName, iName, + "this annotation is restricted") + } + // server-snippet + if annotation, ok := validate(ingress.Annotations, validSnippets, + serverSnippet); !ok { + return fmt.Errorf( + "invalid %s annotation on standby environment, route %s, ingress %s: %s", + serverSnippet, rName, iName, annotation) + } + } + } + } + } return nil } } diff --git a/internal/lagoonyml/testdata/invalid.0.lagoon.yml b/internal/lagoonyml/testdata/invalid.0.lagoon.yml index cd3e568..e133b01 100644 --- a/internal/lagoonyml/testdata/invalid.0.lagoon.yml +++ b/internal/lagoonyml/testdata/invalid.0.lagoon.yml @@ -16,3 +16,22 @@ environments: if ($request_uri !~ \"^/abc\") { return 301 https://dev.example.com$request_uri; } + +production_routes: + active: + routes: + - nginx: + - "www.example.com": + tls-acme: true + insecure: Redirect + annotations: + nginx.ingress.kubernetes.io/server-snippet: | + if ($request_uri !~ \"^/abc\") { + return 301 https://dev.example.com$request_uri; + } + standby: + routes: + - nginx: + - "www.standby.example.com": + tls-acme: "false" + insecure: Redirect \ No newline at end of file diff --git a/internal/lagoonyml/testdata/valid.broken.0.lagoon.yml b/internal/lagoonyml/testdata/valid.broken.0.lagoon.yml new file mode 100644 index 0000000..ff766b3 --- /dev/null +++ b/internal/lagoonyml/testdata/valid.broken.0.lagoon.yml @@ -0,0 +1,24 @@ +environments: + cronjobs: + routes: + - name: a cronjob defined as environment + schedule: "* * * * *" + command: echo "broken definition" + service: cli + main: + routes: + - nginx: + - example.com + - "www.example.com": + tls-acme: 'true' + insecure: Redirect + hsts: max-age=31536000 + - "example.com": + annotations: + nginx.ingress.kubernetes.io/server-snippet: | + set_real_ip_from 1.2.3.4/32; + - "dev.example.com": + annotations: + nginx.ingress.kubernetes.io/server-snippet: | + set_real_ip_from 1.2.3.4/32; + add_header Content-type text/plain; \ No newline at end of file