From 8285d335e0d784157bab770f279c460c620abdf6 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 4 Nov 2021 10:36:02 +0800 Subject: [PATCH] feat: add linter profiles to handle deprecated features --- cmd/lagoon-linter/validate.go | 13 ++++- cmd/lagoon-linter/validateconfigmapjson.go | 14 ++++-- internal/lagoonyml/deprecated/lagoon.go | 18 +++++++ internal/lagoonyml/deprecated/lint.go | 35 ++++++++++++++ internal/lagoonyml/deprecated/lint_test.go | 47 +++++++++++++++++++ .../lagoonyml/deprecated/monitoringurls.go | 36 ++++++++++++++ .../deprecated/monitoringurls_test.go | 42 +++++++++++++++++ .../deprecated/testdata/invalid.0.lagoon.yml | 5 ++ .../deprecated/testdata/invalid.1.lagoon.yml | 7 +++ .../deprecated/testdata/valid.0.lagoon.yml | 18 +++++++ internal/lagoonyml/{ => required}/lagoon.go | 2 +- internal/lagoonyml/{ => required}/lint.go | 12 +++-- .../lagoonyml/{ => required}/lint_test.go | 6 +-- .../{ => required}/routeannotation.go | 32 ++++++------- .../{ => required}/routeannotation_test.go | 6 +-- .../testdata/invalid.0.lagoon.yml | 0 .../testdata/invalid.1.lagoon.yml | 0 .../testdata/invalid.2.lagoon.yml | 0 .../testdata/invalid.3.lagoon.yml | 0 .../testdata/valid.0.lagoon.yml | 0 .../testdata/valid.1.lagoon.yml | 0 .../testdata/valid.2.lagoon.yml | 0 .../testdata/valid.3.lagoon.yml | 0 .../testdata/valid.4.lagoon.yml | 0 .../testdata/valid.5.lagoon.yml | 0 .../testdata/valid.6.lagoon.yml | 0 26 files changed, 261 insertions(+), 32 deletions(-) create mode 100644 internal/lagoonyml/deprecated/lagoon.go create mode 100644 internal/lagoonyml/deprecated/lint.go create mode 100644 internal/lagoonyml/deprecated/lint_test.go create mode 100644 internal/lagoonyml/deprecated/monitoringurls.go create mode 100644 internal/lagoonyml/deprecated/monitoringurls_test.go create mode 100644 internal/lagoonyml/deprecated/testdata/invalid.0.lagoon.yml create mode 100644 internal/lagoonyml/deprecated/testdata/invalid.1.lagoon.yml create mode 100644 internal/lagoonyml/deprecated/testdata/valid.0.lagoon.yml rename internal/lagoonyml/{ => required}/lagoon.go (98%) rename internal/lagoonyml/{ => required}/lint.go (67%) rename internal/lagoonyml/{ => required}/lint_test.go (91%) rename internal/lagoonyml/{ => required}/routeannotation.go (81%) rename internal/lagoonyml/{ => required}/routeannotation_test.go (97%) rename internal/lagoonyml/{ => required}/testdata/invalid.0.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/invalid.1.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/invalid.2.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/invalid.3.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.0.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.1.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.2.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.3.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.4.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.5.lagoon.yml (100%) rename internal/lagoonyml/{ => required}/testdata/valid.6.lagoon.yml (100%) diff --git a/cmd/lagoon-linter/validate.go b/cmd/lagoon-linter/validate.go index 940df6f..a240659 100644 --- a/cmd/lagoon-linter/validate.go +++ b/cmd/lagoon-linter/validate.go @@ -4,12 +4,14 @@ import ( "fmt" "os" - "github.com/uselagoon/lagoon-linter/internal/lagoonyml" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/deprecated" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/required" ) // ValidateCmd represents the validate command. type ValidateCmd struct { LagoonYAML string `kong:"default='.lagoon.yml',help='Specify the Lagoon YAML file.'"` + Profile string `kong:"default='required',enum='required,deprecated',help='Set the linting profile (required,deprecated)'"` } // Run the validation of the Lagoon YAML. @@ -18,5 +20,12 @@ func (cmd *ValidateCmd) Run() error { if err != nil { return fmt.Errorf("couldn't read %v: %v", cmd.LagoonYAML, err) } - return lagoonyml.Lint(rawYAML, lagoonyml.RouteAnnotation()) + switch cmd.Profile { + case "required": + return required.Lint(rawYAML, required.DefaultLinters()) + case "deprecated": + return deprecated.Lint(rawYAML, deprecated.DefaultLinters()) + default: + return fmt.Errorf("invalid profile: %v", cmd.Profile) + } } diff --git a/cmd/lagoon-linter/validateconfigmapjson.go b/cmd/lagoon-linter/validateconfigmapjson.go index 77d4483..d65993b 100644 --- a/cmd/lagoon-linter/validateconfigmapjson.go +++ b/cmd/lagoon-linter/validateconfigmapjson.go @@ -5,12 +5,14 @@ import ( "fmt" "os" - "github.com/uselagoon/lagoon-linter/internal/lagoonyml" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/deprecated" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/required" ) // ValidateConfigMapJSONCmd represents the validate command. type ValidateConfigMapJSONCmd struct { ConfigMapJSON string `kong:"default='configmap.json',help='Specify the configmap JSON file dump.'"` + Profile string `kong:"default='required',enum='required,deprecated',help='Set the linting profile (required,deprecated)'"` } // ConfigMap represents an individual configmap. @@ -39,8 +41,14 @@ func (cmd *ValidateConfigMapJSONCmd) Run() error { // lint it for _, cm := range cml.ConfigMaps { if lagoonYAML, ok := cm.Data[".lagoon.yml"]; ok { - err := lagoonyml.Lint([]byte(lagoonYAML), - lagoonyml.RouteAnnotation()) + switch cmd.Profile { + case "required": + err = required.Lint([]byte(lagoonYAML), required.DefaultLinters()) + case "deprecated": + err = deprecated.Lint([]byte(lagoonYAML), deprecated.DefaultLinters()) + default: + return fmt.Errorf("invalid profile: %v", cmd.Profile) + } if err != nil { fmt.Printf("bad .lagoon.yml: %s: %v\n", cm.Metadata["namespace"], err) } diff --git a/internal/lagoonyml/deprecated/lagoon.go b/internal/lagoonyml/deprecated/lagoon.go new file mode 100644 index 0000000..3f85434 --- /dev/null +++ b/internal/lagoonyml/deprecated/lagoon.go @@ -0,0 +1,18 @@ +package deprecated + +// Environment represents a Lagoon environment. +type Environment struct { + MonitoringURLs interface{} `json:"monitoring_urls"` +} + +// ProductionRoutes represents an active/standby configuration. +type ProductionRoutes struct { + Active *Environment `json:"active"` + Standby *Environment `json:"standby"` +} + +// Lagoon represents the .lagoon.yml file. +type Lagoon struct { + Environments map[string]Environment `json:"environments"` + ProductionRoutes *ProductionRoutes `json:"production_routes"` +} diff --git a/internal/lagoonyml/deprecated/lint.go b/internal/lagoonyml/deprecated/lint.go new file mode 100644 index 0000000..b800d61 --- /dev/null +++ b/internal/lagoonyml/deprecated/lint.go @@ -0,0 +1,35 @@ +package deprecated + +import ( + "fmt" + + "github.com/uselagoon/lagoon-linter/internal/lagoonyml" + "sigs.k8s.io/yaml" +) + +// Linter validates the given Lagoon struct. +type Linter func(*Lagoon) error + +// DefaultLinters returns the list of default linters for this profile. +func DefaultLinters() []Linter { + return []Linter{MonitoringURLs} +} + +// Lint takes a byte slice containing raw YAML and applies `.lagoon.yml` lint +// policy to it. Lint returns an error of type ErrLint if it finds problems +// with the file, a regular error if something else went wrong, and nil if the +// `.lagoon.yml` is valid. +func Lint(rawYAML []byte, linters []Linter) error { + var l Lagoon + if err := yaml.Unmarshal(rawYAML, &l); err != nil { + return fmt.Errorf("couldn't unmarshal YAML: %v", err) + } + for _, linter := range linters { + if err := linter(&l); err != nil { + return &lagoonyml.ErrLint{ + Detail: err.Error(), + } + } + } + return nil +} diff --git a/internal/lagoonyml/deprecated/lint_test.go b/internal/lagoonyml/deprecated/lint_test.go new file mode 100644 index 0000000..d381864 --- /dev/null +++ b/internal/lagoonyml/deprecated/lint_test.go @@ -0,0 +1,47 @@ +package deprecated_test + +import ( + "os" + "testing" + + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/deprecated" +) + +func TestLint(t *testing.T) { + var testCases = map[string]struct { + input string + valid bool + }{ + "valid.0.lagoon.yml": { + input: "testdata/valid.0.lagoon.yml", + valid: true, + }, + "invalid standard monitoring_urls": { + input: "testdata/invalid.0.lagoon.yml", + valid: false, + }, + "invalid weird monitoring_urls": { + input: "testdata/invalid.1.lagoon.yml", + valid: false, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + rawYAML, err := os.ReadFile(tc.input) + if err != nil { + tt.Fatalf("couldn't read %v: %v", tc.input, err) + } + err = deprecated.Lint(rawYAML, deprecated.DefaultLinters()) + if tc.valid { + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + } else { + tt.Log(err) + if err == nil { + tt.Fatalf("expected error, but got nil") + } + } + }) + } +} diff --git a/internal/lagoonyml/deprecated/monitoringurls.go b/internal/lagoonyml/deprecated/monitoringurls.go new file mode 100644 index 0000000..eb481aa --- /dev/null +++ b/internal/lagoonyml/deprecated/monitoringurls.go @@ -0,0 +1,36 @@ +package deprecated + +import ( + "fmt" +) + +// validateEnvironment returns an error if the annotations on the environment +// are invalid, and nil otherwise. +func validateEnvironment(e *Environment) error { + if e.MonitoringURLs != nil { + return fmt.Errorf("deprecated monitoring_urls directive") + } + return nil +} + +// MonitoringURLs checks for deprecated monitoring URLs on a Lagoon environment. +func MonitoringURLs(l *Lagoon) error { + for eName, e := range l.Environments { + if err := validateEnvironment(&e); err != nil { + return fmt.Errorf("environment %s: %v", eName, err) + } + } + if l.ProductionRoutes != nil { + if l.ProductionRoutes.Active != nil { + if err := validateEnvironment(l.ProductionRoutes.Active); err != nil { + return fmt.Errorf("active environment: %v", err) + } + } + if l.ProductionRoutes.Standby != nil { + if err := validateEnvironment(l.ProductionRoutes.Standby); err != nil { + return fmt.Errorf("standby environment: %v", err) + } + } + } + return nil +} diff --git a/internal/lagoonyml/deprecated/monitoringurls_test.go b/internal/lagoonyml/deprecated/monitoringurls_test.go new file mode 100644 index 0000000..845cdbd --- /dev/null +++ b/internal/lagoonyml/deprecated/monitoringurls_test.go @@ -0,0 +1,42 @@ +package deprecated + +import ( + "testing" +) + +func TestMonitoringURLS(t *testing.T) { + var testCases = map[string]struct { + input interface{} + valid bool + }{ + "valid monitoring_urls absent": { + input: nil, + valid: true, + }, + "invalid monitoring_urls present": { + input: []string{"example.com", "www.example.com"}, + valid: false, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + l := Lagoon{ + Environments: map[string]Environment{ + "testenv": { + MonitoringURLs: tc.input, + }, + }, + } + err := MonitoringURLs(&l) + if tc.valid { + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + } else { + if err == nil { + tt.Fatalf("expected error, but got nil") + } + } + }) + } +} diff --git a/internal/lagoonyml/deprecated/testdata/invalid.0.lagoon.yml b/internal/lagoonyml/deprecated/testdata/invalid.0.lagoon.yml new file mode 100644 index 0000000..b51718d --- /dev/null +++ b/internal/lagoonyml/deprecated/testdata/invalid.0.lagoon.yml @@ -0,0 +1,5 @@ +environments: + main: + monitoring_urls: + - "https://www.example.com" + - "https://www.example.com/special_page" diff --git a/internal/lagoonyml/deprecated/testdata/invalid.1.lagoon.yml b/internal/lagoonyml/deprecated/testdata/invalid.1.lagoon.yml new file mode 100644 index 0000000..17ef950 --- /dev/null +++ b/internal/lagoonyml/deprecated/testdata/invalid.1.lagoon.yml @@ -0,0 +1,7 @@ +environments: + main: + monitoring_urls: + weird: + structure: + - "https://www.example.com" + - "https://www.example.com/special_page" diff --git a/internal/lagoonyml/deprecated/testdata/valid.0.lagoon.yml b/internal/lagoonyml/deprecated/testdata/valid.0.lagoon.yml new file mode 100644 index 0000000..38dd9ce --- /dev/null +++ b/internal/lagoonyml/deprecated/testdata/valid.0.lagoon.yml @@ -0,0 +1,18 @@ +environments: + 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; diff --git a/internal/lagoonyml/lagoon.go b/internal/lagoonyml/required/lagoon.go similarity index 98% rename from internal/lagoonyml/lagoon.go rename to internal/lagoonyml/required/lagoon.go index 33a79b9..2b527bc 100644 --- a/internal/lagoonyml/lagoon.go +++ b/internal/lagoonyml/required/lagoon.go @@ -1,4 +1,4 @@ -package lagoonyml +package required import "encoding/json" diff --git a/internal/lagoonyml/lint.go b/internal/lagoonyml/required/lint.go similarity index 67% rename from internal/lagoonyml/lint.go rename to internal/lagoonyml/required/lint.go index ec33ce8..ce8cf3b 100644 --- a/internal/lagoonyml/lint.go +++ b/internal/lagoonyml/required/lint.go @@ -1,26 +1,32 @@ -package lagoonyml +package required import ( "fmt" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml" "sigs.k8s.io/yaml" ) // Linter validates the given Lagoon struct. type Linter func(*Lagoon) error +// DefaultLinters returns the list of default linters for this profile. +func DefaultLinters() []Linter { + return []Linter{RouteAnnotation} +} + // Lint takes a byte slice containing raw YAML and applies `.lagoon.yml` lint // policy to it. Lint returns an error of type ErrLint if it finds problems // with the file, a regular error if something else went wrong, and nil if the // `.lagoon.yml` is valid. -func Lint(rawYAML []byte, linters ...Linter) error { +func Lint(rawYAML []byte, linters []Linter) error { var l Lagoon if err := yaml.Unmarshal(rawYAML, &l); err != nil { return fmt.Errorf("couldn't unmarshal YAML: %v", err) } for _, linter := range linters { if err := linter(&l); err != nil { - return &ErrLint{ + return &lagoonyml.ErrLint{ Detail: err.Error(), } } diff --git a/internal/lagoonyml/lint_test.go b/internal/lagoonyml/required/lint_test.go similarity index 91% rename from internal/lagoonyml/lint_test.go rename to internal/lagoonyml/required/lint_test.go index 9398045..3d1f6c1 100644 --- a/internal/lagoonyml/lint_test.go +++ b/internal/lagoonyml/required/lint_test.go @@ -1,10 +1,10 @@ -package lagoonyml_test +package required_test import ( "os" "testing" - "github.com/uselagoon/lagoon-linter/internal/lagoonyml" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml/required" ) func TestLint(t *testing.T) { @@ -63,7 +63,7 @@ func TestLint(t *testing.T) { if err != nil { tt.Fatalf("couldn't read %v: %v", tc.input, err) } - err = lagoonyml.Lint(rawYAML, lagoonyml.RouteAnnotation()) + err = required.Lint(rawYAML, required.DefaultLinters()) if tc.valid { if err != nil { tt.Fatalf("unexpected error %v", err) diff --git a/internal/lagoonyml/routeannotation.go b/internal/lagoonyml/required/routeannotation.go similarity index 81% rename from internal/lagoonyml/routeannotation.go rename to internal/lagoonyml/required/routeannotation.go index 30916b1..77ba711 100644 --- a/internal/lagoonyml/routeannotation.go +++ b/internal/lagoonyml/required/routeannotation.go @@ -1,4 +1,4 @@ -package lagoonyml +package required import ( "fmt" @@ -80,25 +80,23 @@ func validateEnvironment(e *Environment) error { } // RouteAnnotation checks for valid annotations on defined routes. -func RouteAnnotation() Linter { - return func(l *Lagoon) error { - for eName, e := range l.Environments { - if err := validateEnvironment(&e); err != nil { - return fmt.Errorf("environment %s: %v", eName, err) - } +func RouteAnnotation(l *Lagoon) error { + for eName, e := range l.Environments { + if err := validateEnvironment(&e); err != nil { + return fmt.Errorf("environment %s: %v", eName, err) } - if l.ProductionRoutes != nil { - if l.ProductionRoutes.Active != nil { - if err := validateEnvironment(l.ProductionRoutes.Active); err != nil { - return fmt.Errorf("active environment: %v", err) - } + } + if l.ProductionRoutes != nil { + if l.ProductionRoutes.Active != nil { + if err := validateEnvironment(l.ProductionRoutes.Active); err != nil { + return fmt.Errorf("active environment: %v", err) } - if l.ProductionRoutes.Standby != nil { - if err := validateEnvironment(l.ProductionRoutes.Standby); err != nil { - return fmt.Errorf("standby environment: %v", err) - } + } + if l.ProductionRoutes.Standby != nil { + if err := validateEnvironment(l.ProductionRoutes.Standby); err != nil { + return fmt.Errorf("standby environment: %v", err) } } - return nil } + return nil } diff --git a/internal/lagoonyml/routeannotation_test.go b/internal/lagoonyml/required/routeannotation_test.go similarity index 97% rename from internal/lagoonyml/routeannotation_test.go rename to internal/lagoonyml/required/routeannotation_test.go index 829c3ee..15ced0f 100644 --- a/internal/lagoonyml/routeannotation_test.go +++ b/internal/lagoonyml/required/routeannotation_test.go @@ -1,4 +1,4 @@ -package lagoonyml +package required import ( "testing" @@ -82,7 +82,7 @@ func TestServerSnippet(t *testing.T) { }, }, } - err := RouteAnnotation()(&l) + err := RouteAnnotation(&l) if tc.valid { if err != nil { tt.Fatalf("unexpected error %v", err) @@ -142,7 +142,7 @@ func TestRestrictedSnippets(t *testing.T) { }, }, } - err := RouteAnnotation()(&l) + err := RouteAnnotation(&l) if tc.valid { if err != nil { tt.Fatalf("unexpected error %v", err) diff --git a/internal/lagoonyml/testdata/invalid.0.lagoon.yml b/internal/lagoonyml/required/testdata/invalid.0.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/invalid.0.lagoon.yml rename to internal/lagoonyml/required/testdata/invalid.0.lagoon.yml diff --git a/internal/lagoonyml/testdata/invalid.1.lagoon.yml b/internal/lagoonyml/required/testdata/invalid.1.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/invalid.1.lagoon.yml rename to internal/lagoonyml/required/testdata/invalid.1.lagoon.yml diff --git a/internal/lagoonyml/testdata/invalid.2.lagoon.yml b/internal/lagoonyml/required/testdata/invalid.2.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/invalid.2.lagoon.yml rename to internal/lagoonyml/required/testdata/invalid.2.lagoon.yml diff --git a/internal/lagoonyml/testdata/invalid.3.lagoon.yml b/internal/lagoonyml/required/testdata/invalid.3.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/invalid.3.lagoon.yml rename to internal/lagoonyml/required/testdata/invalid.3.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.0.lagoon.yml b/internal/lagoonyml/required/testdata/valid.0.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.0.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.0.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.1.lagoon.yml b/internal/lagoonyml/required/testdata/valid.1.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.1.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.1.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.2.lagoon.yml b/internal/lagoonyml/required/testdata/valid.2.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.2.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.2.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.3.lagoon.yml b/internal/lagoonyml/required/testdata/valid.3.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.3.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.3.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.4.lagoon.yml b/internal/lagoonyml/required/testdata/valid.4.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.4.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.4.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.5.lagoon.yml b/internal/lagoonyml/required/testdata/valid.5.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.5.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.5.lagoon.yml diff --git a/internal/lagoonyml/testdata/valid.6.lagoon.yml b/internal/lagoonyml/required/testdata/valid.6.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/valid.6.lagoon.yml rename to internal/lagoonyml/required/testdata/valid.6.lagoon.yml