diff --git a/cmd/template_ingress_test.go b/cmd/template_ingress_test.go index 59a30962..20a44562 100644 --- a/cmd/template_ingress_test.go +++ b/cmd/template_ingress_test.go @@ -432,6 +432,44 @@ func TestTemplateRoutes(t *testing.T) { }, want: "../test-resources/template-ingress/test20-results", }, + { + name: "test21 alternative names", + args: args{ + alertContact: "alertcontact", + statusPageID: "statuspageid", + projectName: "example-project", + environmentName: "main", + environmentType: "production", + buildType: "branch", + standbyEnvironment: "main2", + lagoonVersion: "v2.7.x", + branch: "main", + projectVars: `[{"name":"LAGOON_SYSTEM_ROUTER_PATTERN","value":"${service}-${project}-${environment}.example.com","scope":"internal_system"}]`, + envVars: `[]`, + secretPrefix: "fastly-api-", + lagoonYAML: "../test-resources/template-ingress/test21/lagoon.yml", + templatePath: "../test-resources/template-ingress/output", + }, + want: "../test-resources/template-ingress/test21-results", + }, + { + name: "test22 check wildcard", + args: args{ + alertContact: "alertcontact", + statusPageID: "statuspageid", + projectName: "example-project", + environmentName: "main", + environmentType: "production", + buildType: "branch", + lagoonVersion: "v2.7.x", + branch: "main", + projectVars: `[{"name":"LAGOON_SYSTEM_ROUTER_PATTERN","value":"${service}-${project}-${environment}.example.com","scope":"internal_system"}]`, + envVars: `[]`, + lagoonYAML: "../test-resources/template-ingress/test22/lagoon.yml", + templatePath: "../test-resources/template-ingress/output", + }, + want: "../test-resources/template-ingress/test22-results", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/generator/ingress.go b/internal/generator/ingress.go index 2f8715f8..c942349a 100644 --- a/internal/generator/ingress.go +++ b/internal/generator/ingress.go @@ -76,7 +76,10 @@ func generateRoutes( // generate the templates for these independently of any previously generated routes, // this WILL overwrite previously created templates ensuring that anything defined in the `production_routes` // section are created correctly ensuring active/standby will work - *activeStanbyRoutes = generateActiveStandbyRoutes(lagoonEnvVars, lYAML, buildValues) + *activeStanbyRoutes, err = generateActiveStandbyRoutes(lagoonEnvVars, lYAML, buildValues) + if err != nil { + return "", []string{}, []string{}, fmt.Errorf("couldn't generate and merge routes: %v", err) + } // get the first route from the list of routes, replace the previous one if necessary if len(activeStanbyRoutes.Routes) > 0 { // if primary != "" { @@ -244,14 +247,17 @@ func generateActiveStandbyRoutes( envVars []lagoon.EnvironmentVariable, lagoonYAML lagoon.YAML, buildValues BuildValues, -) lagoon.RoutesV2 { +) (lagoon.RoutesV2, error) { activeStanbyRoutes := &lagoon.RoutesV2{} if lagoonYAML.ProductionRoutes != nil { if buildValues.IsActiveEnvironment == true { if lagoonYAML.ProductionRoutes.Active != nil { if lagoonYAML.ProductionRoutes.Active.Routes != nil { for _, routeMap := range lagoonYAML.ProductionRoutes.Active.Routes { - lagoon.GenerateRoutesV2(activeStanbyRoutes, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, true) + err := lagoon.GenerateRoutesV2(activeStanbyRoutes, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, true) + if err != nil { + return *activeStanbyRoutes, err + } } } } @@ -260,13 +266,16 @@ func generateActiveStandbyRoutes( if lagoonYAML.ProductionRoutes.Standby != nil { if lagoonYAML.ProductionRoutes.Standby.Routes != nil { for _, routeMap := range lagoonYAML.ProductionRoutes.Standby.Routes { - lagoon.GenerateRoutesV2(activeStanbyRoutes, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, true) + err := lagoon.GenerateRoutesV2(activeStanbyRoutes, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, true) + if err != nil { + return *activeStanbyRoutes, err + } } } } } } - return *activeStanbyRoutes + return *activeStanbyRoutes, nil } // getRoutesFromEnvVar will collect the value of the LAGOON_ROUTES_JSON @@ -305,9 +314,15 @@ func generateAndMerge( // otherwise it just uses the default environment name for _, routeMap := range lagoonYAML.Environments[buildValues.Branch].Routes { - lagoon.GenerateRoutesV2(n, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, false) + err := lagoon.GenerateRoutesV2(n, routeMap, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix, false) + if err != nil { + return *n, err + } } // merge routes from the API on top of the routes from the `.lagoon.yml` - mainRoutes := lagoon.MergeRoutesV2(*n, api, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix) + mainRoutes, err := lagoon.MergeRoutesV2(*n, api, envVars, buildValues.IngressClass, buildValues.FastlyAPISecretPrefix) + if err != nil { + return *n, err + } return mainRoutes, nil } diff --git a/internal/generator/ingress_test.go b/internal/generator/ingress_test.go index 972c3927..225f085c 100644 --- a/internal/generator/ingress_test.go +++ b/internal/generator/ingress_test.go @@ -162,36 +162,40 @@ func Test_generateAndMerge(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "a.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/bypass-cache", + Domain: "a.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/bypass-cache", + AlternativeNames: []string{}, }, { - Domain: "b.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", + Domain: "b.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + AlternativeNames: []string{}, }, { - Domain: "c.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", + Domain: "c.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + AlternativeNames: []string{}, }, { - Domain: "test1.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - MonitoringPath: "/bypass-cache", - Insecure: helpers.StrPtr("Redirect"), - Annotations: map[string]string{}, + Domain: "test1.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + MonitoringPath: "/bypass-cache", + Insecure: helpers.StrPtr("Redirect"), + Annotations: map[string]string{}, + AlternativeNames: []string{}, }, }, }, @@ -223,20 +227,22 @@ func Test_generateAndMerge(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "test1.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - MonitoringPath: "/bypass-cache", - Insecure: helpers.StrPtr("Redirect"), - Annotations: map[string]string{}, + Domain: "test1.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + MonitoringPath: "/bypass-cache", + Insecure: helpers.StrPtr("Redirect"), + Annotations: map[string]string{}, + AlternativeNames: []string{}, }, { - Domain: "a.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/bypass-cache", + Domain: "a.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/bypass-cache", + AlternativeNames: []string{}, }, }, }, @@ -293,46 +299,50 @@ func Test_generateAndMerge(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "a.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/bypass-cache", - IngressClass: "nginx", + Domain: "a.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/bypass-cache", + IngressClass: "nginx", + AlternativeNames: []string{}, }, { - Domain: "b.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", - IngressClass: "nginx", + Domain: "b.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + IngressClass: "nginx", + AlternativeNames: []string{}, }, { - Domain: "c.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", - IngressClass: "nginx", + Domain: "c.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + IngressClass: "nginx", + AlternativeNames: []string{}, }, { - Domain: "test1.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - MonitoringPath: "/bypass-cache", - Insecure: helpers.StrPtr("Redirect"), - Annotations: map[string]string{}, - IngressClass: "nginx", + Domain: "test1.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + MonitoringPath: "/bypass-cache", + Insecure: helpers.StrPtr("Redirect"), + Annotations: map[string]string{}, + IngressClass: "nginx", + AlternativeNames: []string{}, }, }, }, }, { - name: "test3 - generate routes from lagoon yaml and merge ones from api with hsts", + name: "test4 - generate routes from lagoon yaml and merge ones from api with hsts", args: args{ buildValues: BuildValues{ Branch: "main", @@ -373,18 +383,96 @@ func Test_generateAndMerge(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "a.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(false), - Annotations: map[string]string{}, - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/bypass-cache", - IngressClass: "nginx", - HSTSEnabled: helpers.BoolPtr(true), - HSTSMaxAge: 36000, + Domain: "a.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/bypass-cache", + IngressClass: "nginx", + HSTSEnabled: helpers.BoolPtr(true), + HSTSMaxAge: 36000, + AlternativeNames: []string{}, + }, + }, + }, + }, + { + name: "test5 - wildcard with tls-acme false", + args: args{ + buildValues: BuildValues{ + Branch: "main", + IngressClass: "nginx", + }, + lagoonYAML: lagoon.YAML{ + Environments: lagoon.Environments{ + "main": lagoon.Environment{ + Routes: []map[string][]lagoon.Route{ + { + "nginx": { + { + Ingresses: map[string]lagoon.Ingress{ + "a.example.com": { + TLSAcme: helpers.BoolPtr(false), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: lagoon.RoutesV2{ + Routes: []lagoon.RouteV2{ + { + Domain: "a.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + IngressClass: "nginx", + AlternativeNames: []string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + { + name: "test6 - wildcard with tls-acme true (should error)", + args: args{ + buildValues: BuildValues{ + Branch: "main", + IngressClass: "nginx", + }, + lagoonYAML: lagoon.YAML{ + Environments: lagoon.Environments{ + "main": lagoon.Environment{ + Routes: []map[string][]lagoon.Route{ + { + "nginx": { + { + Ingresses: map[string]lagoon.Ingress{ + "a.example.com": { + TLSAcme: helpers.BoolPtr(true), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + }, + }, }, }, }, + wantErr: true, + want: lagoon.RoutesV2{ + Routes: nil, + }, }, } for _, tt := range tests { @@ -396,7 +484,7 @@ func Test_generateAndMerge(t *testing.T) { } lValues, _ := json.Marshal(got) wValues, _ := json.Marshal(tt.want) - if !reflect.DeepEqual(string(lValues), string(wValues)) { + if !reflect.DeepEqual(string(lValues), string(wValues)) && !tt.wantErr { t.Errorf("generateAndMerge() = %v, want %v", string(lValues), string(wValues)) } }) @@ -410,9 +498,10 @@ func Test_generateActiveStandbyRoutes(t *testing.T) { buildValues BuildValues } tests := []struct { - name string - args args - want lagoon.RoutesV2 + name string + args args + want lagoon.RoutesV2 + wantErr bool }{ { name: "test1", @@ -440,13 +529,14 @@ func Test_generateActiveStandbyRoutes(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "active.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Migrate: helpers.BoolPtr(true), - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", + Domain: "active.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Migrate: helpers.BoolPtr(true), + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + AlternativeNames: []string{}, }, }, }, @@ -482,20 +572,21 @@ func Test_generateActiveStandbyRoutes(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "active.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Migrate: helpers.BoolPtr(true), - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", - IngressClass: "nginx", + Domain: "active.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Migrate: helpers.BoolPtr(true), + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + IngressClass: "nginx", + AlternativeNames: []string{}, }, }, }, }, { - name: "test2 - with custom ingress class defined", + name: "test3 - with custom ingress class defined", args: args{ buildValues: BuildValues{ IsActiveEnvironment: true, @@ -526,14 +617,95 @@ func Test_generateActiveStandbyRoutes(t *testing.T) { want: lagoon.RoutesV2{ Routes: []lagoon.RouteV2{ { - Domain: "active.example.com", - LagoonService: "nginx", - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - Migrate: helpers.BoolPtr(true), - Insecure: helpers.StrPtr("Redirect"), - MonitoringPath: "/", - IngressClass: "custom-nginx", + Domain: "active.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Migrate: helpers.BoolPtr(true), + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + IngressClass: "custom-nginx", + AlternativeNames: []string{}, + }, + }, + }, + }, + { + name: "test4 - with wildcard and tls-acme true (should error)", + args: args{ + buildValues: BuildValues{ + IngressClass: "nginx", + IsActiveEnvironment: true, + }, + lagoonYAML: lagoon.YAML{ + ProductionRoutes: &lagoon.ProductionRoutes{ + Active: &lagoon.Environment{ + Routes: []map[string][]lagoon.Route{ + { + "nginx": { + { + Ingresses: map[string]lagoon.Ingress{ + "active.example.com": { + TLSAcme: helpers.BoolPtr(true), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + }, + }, + }, + }, + envVars: []lagoon.EnvironmentVariable{}, + }, + wantErr: true, + want: lagoon.RoutesV2{ + Routes: nil, + }, + }, + { + name: "test5 - with wildcard and tls-acme false", + args: args{ + buildValues: BuildValues{ + IngressClass: "nginx", + IsActiveEnvironment: true, + }, + lagoonYAML: lagoon.YAML{ + ProductionRoutes: &lagoon.ProductionRoutes{ + Active: &lagoon.Environment{ + Routes: []map[string][]lagoon.Route{ + { + "nginx": { + { + Ingresses: map[string]lagoon.Ingress{ + "active.example.com": { + TLSAcme: helpers.BoolPtr(false), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + }, + }, + }, + }, + envVars: []lagoon.EnvironmentVariable{}, + }, + want: lagoon.RoutesV2{ + Routes: []lagoon.RouteV2{ + { + Domain: "active.example.com", + LagoonService: "nginx", + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Migrate: helpers.BoolPtr(true), + Insecure: helpers.StrPtr("Redirect"), + MonitoringPath: "/", + IngressClass: "nginx", + AlternativeNames: []string{}, + Wildcard: helpers.BoolPtr(true), }, }, }, @@ -541,10 +713,14 @@ func Test_generateActiveStandbyRoutes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := generateActiveStandbyRoutes(tt.args.envVars, tt.args.lagoonYAML, tt.args.buildValues) + got, err := generateActiveStandbyRoutes(tt.args.envVars, tt.args.lagoonYAML, tt.args.buildValues) + if (err != nil) != tt.wantErr { + t.Errorf("generateAndMerge() error = %v, wantErr %v", err, tt.wantErr) + return + } lValues, _ := json.Marshal(got) wValues, _ := json.Marshal(tt.want) - if !reflect.DeepEqual(string(lValues), string(wValues)) { + if !reflect.DeepEqual(string(lValues), string(wValues)) && !tt.wantErr { t.Errorf("generateAndMerge() = %v, want %v", string(lValues), string(wValues)) } }) diff --git a/internal/lagoon/routes.go b/internal/lagoon/routes.go index 571ba6b6..c4302672 100644 --- a/internal/lagoon/routes.go +++ b/internal/lagoon/routes.go @@ -2,10 +2,13 @@ package lagoon import ( "encoding/json" + "fmt" "reflect" "strconv" + "strings" "github.com/uselagoon/build-deploy-tool/internal/helpers" + "k8s.io/apimachinery/pkg/util/validation" ) // RoutesV2 is the new routes definition @@ -35,6 +38,7 @@ type RouteV2 struct { HSTSIncludeSubdomains *bool `json:"hstsIncludeSubdomains,omitempty"` HSTSPreload *bool `json:"hstsPreload,omitempty"` Autogenerated bool `json:"-"` + Wildcard *bool `json:"wildcard,omitempty"` } // Ingress represents a Lagoon route. @@ -50,6 +54,8 @@ type Ingress struct { HSTSMaxAge int `json:"hstsMaxAge,omitempty"` HSTSIncludeSubdomains *bool `json:"hstsIncludeSubdomains,omitempty"` HSTSPreload *bool `json:"hstsPreload,omitempty"` + AlternativeNames []string `json:"alternativenames,omitempty"` + Wildcard *bool `json:"wildcard,omitempty"` } // Route can be either a string or a map[string]Ingress, so we must @@ -59,7 +65,17 @@ type Route struct { Ingresses map[string]Ingress } -var defaultHSTSMaxAge = 31536000 +// defaults +var ( + defaultHSTSMaxAge = 31536000 + defaultMonitoringPath string = "/" + defaultFastlyService string = "" + defaultFastlyWatch bool = false + defaultInsecure *string = helpers.StrPtr("Redirect") + defaultTLSAcme *bool = helpers.BoolPtr(true) + defaultActiveStandby *bool = helpers.BoolPtr(true) + defaultAnnotations map[string]string = map[string]string{} +) // UnmarshalJSON implements json.Unmarshaler. func (r *Route) UnmarshalJSON(data []byte) error { @@ -98,20 +114,21 @@ func (r *Route) UnmarshalJSON(data []byte) error { } // GenerateRoutesV2 generate routesv2 definitions from lagoon route mappings -func GenerateRoutesV2(genRoutes *RoutesV2, routeMap map[string][]Route, variables []EnvironmentVariable, defaultIngressClass, secretPrefix string, activeStandby bool) { +func GenerateRoutesV2(yamlRoutes *RoutesV2, routeMap map[string][]Route, variables []EnvironmentVariable, defaultIngressClass, secretPrefix string, activeStandby bool) error { for rName, lagoonRoutes := range routeMap { for _, lagoonRoute := range lagoonRoutes { newRoute := RouteV2{} // set the defaults for routes - newRoute.TLSAcme = helpers.BoolPtr(true) - newRoute.Insecure = helpers.StrPtr("Redirect") - newRoute.MonitoringPath = "/" - newRoute.Annotations = map[string]string{} - newRoute.Fastly.ServiceID = "" - newRoute.Fastly.Watch = false + newRoute.TLSAcme = defaultTLSAcme + newRoute.Insecure = defaultInsecure + newRoute.MonitoringPath = defaultMonitoringPath + newRoute.Annotations = defaultAnnotations + newRoute.Fastly.ServiceID = defaultFastlyService + newRoute.Fastly.Watch = defaultFastlyWatch + newRoute.AlternativeNames = []string{} newRoute.IngressClass = defaultIngressClass if activeStandby { - newRoute.Migrate = helpers.BoolPtr(true) + newRoute.Migrate = defaultActiveStandby } if lagoonRoute.Name == "" { // this route from the lagoon route map contains field overrides @@ -130,6 +147,9 @@ func GenerateRoutesV2(genRoutes *RoutesV2, routeMap map[string][]Route, variable if ingress.Insecure != nil { newRoute.Insecure = ingress.Insecure } + if ingress.AlternativeNames != nil { + newRoute.AlternativeNames = ingress.AlternativeNames + } if ingress.IngressClass != "" { newRoute.IngressClass = ingress.IngressClass } @@ -155,6 +175,17 @@ func GenerateRoutesV2(genRoutes *RoutesV2, routeMap map[string][]Route, variable } } // hsts end + + // handle wildcards + if ingress.Wildcard != nil { + newRoute.Wildcard = ingress.Wildcard + if *newRoute.TLSAcme == true && *newRoute.Wildcard == true { + return fmt.Errorf("Route %s has wildcard: true and tls-acme: true, this is not supported", newRoute.Domain) + } + if ingress.AlternativeNames != nil && *newRoute.Wildcard == true { + return fmt.Errorf("Route %s has wildcard: true and alternativenames defined, this is not supported", newRoute.Domain) + } + } } } else { // this route is just a domain @@ -168,135 +199,150 @@ func GenerateRoutesV2(genRoutes *RoutesV2, routeMap map[string][]Route, variable //@TODO: error handling } - genRoutes.Routes = append(genRoutes.Routes, newRoute) + // validate the domain earlier and fail if it is invalid + if err := validation.IsDNS1123Subdomain(strings.ToLower(newRoute.Domain)); err != nil { + return fmt.Errorf("Route %s in .lagoon.yml is not valid: %v", newRoute.Domain, err) + } + yamlRoutes.Routes = append(yamlRoutes.Routes, newRoute) } } + return nil } // MergeRoutesV2 merge routes from the API onto the previously generated routes. -func MergeRoutesV2(genRoutes RoutesV2, apiRoutes RoutesV2, variables []EnvironmentVariable, defaultIngressClass, secretPrefix string) RoutesV2 { - finalRoutes := RoutesV2{} +func MergeRoutesV2(yamlRoutes RoutesV2, apiRoutes RoutesV2, variables []EnvironmentVariable, defaultIngressClass, secretPrefix string) (RoutesV2, error) { + firstRoundRoutes := RoutesV2{} existsInAPI := false // replace any routes from the lagoon yaml with ones from the api // this only modifies ones that exist in lagoon yaml - for _, route := range genRoutes.Routes { - add := RouteV2{} - for _, aRoute := range apiRoutes.Routes { - if aRoute.Domain == route.Domain { - existsInAPI = true - add = aRoute - add.Fastly = aRoute.Fastly - if aRoute.TLSAcme != nil { - add.TLSAcme = aRoute.TLSAcme - } else { - add.TLSAcme = helpers.BoolPtr(true) - } - if aRoute.Insecure != nil { - add.Insecure = aRoute.Insecure - } else { - add.Insecure = helpers.StrPtr("Redirect") - } - if aRoute.Annotations != nil { - add.Annotations = aRoute.Annotations - } else { - add.Annotations = map[string]string{} - } - if aRoute.IngressClass != "" { - add.IngressClass = aRoute.IngressClass - } else { - add.IngressClass = defaultIngressClass - } - - // handle hsts here - if aRoute.HSTSEnabled != nil { - add.HSTSEnabled = aRoute.HSTSEnabled - } - if aRoute.HSTSIncludeSubdomains != nil { - add.HSTSIncludeSubdomains = aRoute.HSTSIncludeSubdomains - } - if aRoute.HSTSPreload != nil { - add.HSTSPreload = aRoute.HSTSPreload + for _, route := range yamlRoutes.Routes { + routeAdd := RouteV2{} + // validate the domain earlier and fail if it is invalid + if err := validation.IsDNS1123Subdomain(strings.ToLower(route.Domain)); err != nil { + return firstRoundRoutes, fmt.Errorf("Route %s in .lagoon.yml is not valid: %v", route.Domain, err) + } + for _, apiRoute := range apiRoutes.Routes { + if apiRoute.Domain == route.Domain { + // validate the domain earlier and fail if it is invalid + if err := validation.IsDNS1123Subdomain(strings.ToLower(apiRoute.Domain)); err != nil { + return firstRoundRoutes, fmt.Errorf("Route %s in API defined routes is not valid: %v", apiRoute.Domain, err) } - if aRoute.HSTSMaxAge > 0 { - add.HSTSMaxAge = aRoute.HSTSMaxAge - } else { - if add.HSTSEnabled != nil && *add.HSTSEnabled { - add.HSTSMaxAge = defaultHSTSMaxAge // set default hsts value if one not provided - } + existsInAPI = true + var err error + routeAdd, err = handleAPIRoute(defaultIngressClass, apiRoute) + if err != nil { + return firstRoundRoutes, err } - // hsts end } } if existsInAPI { - finalRoutes.Routes = append(finalRoutes.Routes, add) + firstRoundRoutes.Routes = append(firstRoundRoutes.Routes, routeAdd) existsInAPI = false } else { - finalRoutes.Routes = append(finalRoutes.Routes, route) + + if route.AlternativeNames == nil { + route.AlternativeNames = []string{} + } + firstRoundRoutes.Routes = append(firstRoundRoutes.Routes, route) } } // add any that exist in the api only to the final routes list - for _, aRoute := range apiRoutes.Routes { - add := aRoute - add.Fastly = aRoute.Fastly - if aRoute.TLSAcme != nil { - add.TLSAcme = aRoute.TLSAcme - } else { - add.TLSAcme = helpers.BoolPtr(true) - } - if aRoute.Insecure != nil { - add.Insecure = aRoute.Insecure - } else { - add.Insecure = helpers.StrPtr("Redirect") - } - if aRoute.Annotations != nil { - add.Annotations = aRoute.Annotations - } else { - add.Annotations = map[string]string{} - } - if aRoute.IngressClass != "" { - add.IngressClass = aRoute.IngressClass - } else { - add.IngressClass = defaultIngressClass + for _, apiRoute := range apiRoutes.Routes { + if err := validation.IsDNS1123Subdomain(strings.ToLower(apiRoute.Domain)); err != nil { + return firstRoundRoutes, fmt.Errorf("Route %s in API defined routes is not valid: %v", apiRoute.Domain, err) } - // handle hsts here - if aRoute.HSTSEnabled != nil { - add.HSTSEnabled = aRoute.HSTSEnabled - } - if aRoute.HSTSIncludeSubdomains != nil { - add.HSTSIncludeSubdomains = aRoute.HSTSIncludeSubdomains - } - if aRoute.HSTSPreload != nil { - add.HSTSPreload = aRoute.HSTSPreload - } - if aRoute.HSTSMaxAge > 0 { - add.HSTSMaxAge = aRoute.HSTSMaxAge - } else { - if add.HSTSEnabled != nil && *add.HSTSEnabled { - add.HSTSMaxAge = defaultHSTSMaxAge // set default hsts value if one not provided - } + routeAdd, err := handleAPIRoute(defaultIngressClass, apiRoute) + if err != nil { + return firstRoundRoutes, err } - // hsts end - for _, route := range finalRoutes.Routes { - if aRoute.Domain == route.Domain { + for _, route := range firstRoundRoutes.Routes { + if apiRoute.Domain == route.Domain { existsInAPI = true } } if existsInAPI { existsInAPI = false } else { - finalRoutes.Routes = append(finalRoutes.Routes, add) + firstRoundRoutes.Routes = append(firstRoundRoutes.Routes, routeAdd) } } - finalRoutes2 := RoutesV2{} - for _, fRoute := range finalRoutes.Routes { + + // generate the final routes to provide back as "the" route list for this environment + finalRoutes := RoutesV2{} + for _, fRoute := range firstRoundRoutes.Routes { // generate the fastly configuration for this route if required err := GenerateFastlyConfiguration(&fRoute.Fastly, "", fRoute.Fastly.ServiceID, fRoute.Domain, secretPrefix, variables) if err != nil { //@TODO: error handling } - finalRoutes2.Routes = append(finalRoutes2.Routes, fRoute) + fRoute.Domain = strings.ToLower(fRoute.Domain) + finalRoutes.Routes = append(finalRoutes.Routes, fRoute) + } + return finalRoutes, nil +} + +// handleAPIRoute handles setting the defaults for API defined routes +// main lagoon.yml defaults are handled in `GenerateRoutesV2` function +func handleAPIRoute(defaultIngressClass string, apiRoute RouteV2) (RouteV2, error) { + routeAdd := apiRoute + // copy in the apiroute fastly configuration + routeAdd.Fastly = apiRoute.Fastly + if apiRoute.TLSAcme != nil { + routeAdd.TLSAcme = apiRoute.TLSAcme + } else { + routeAdd.TLSAcme = defaultTLSAcme + } + if apiRoute.Insecure != nil { + routeAdd.Insecure = apiRoute.Insecure + } else { + routeAdd.Insecure = defaultInsecure + } + if apiRoute.Annotations != nil { + routeAdd.Annotations = apiRoute.Annotations + } else { + routeAdd.Annotations = defaultAnnotations + } + if apiRoute.AlternativeNames != nil { + routeAdd.AlternativeNames = apiRoute.AlternativeNames + } else { + routeAdd.AlternativeNames = []string{} + } + if apiRoute.IngressClass != "" { + routeAdd.IngressClass = apiRoute.IngressClass + } else { + routeAdd.IngressClass = defaultIngressClass + } + + // handle hsts here + if apiRoute.HSTSEnabled != nil { + routeAdd.HSTSEnabled = apiRoute.HSTSEnabled + } + if apiRoute.HSTSIncludeSubdomains != nil { + routeAdd.HSTSIncludeSubdomains = apiRoute.HSTSIncludeSubdomains + } + if apiRoute.HSTSPreload != nil { + routeAdd.HSTSPreload = apiRoute.HSTSPreload + } + if apiRoute.HSTSMaxAge > 0 { + routeAdd.HSTSMaxAge = apiRoute.HSTSMaxAge + } else { + if routeAdd.HSTSEnabled != nil && *routeAdd.HSTSEnabled { + routeAdd.HSTSMaxAge = defaultHSTSMaxAge // set default hsts value if one not provided + } + } + // hsts end + + // handle wildcards + if apiRoute.Wildcard != nil { + routeAdd.Wildcard = apiRoute.Wildcard + if *routeAdd.TLSAcme == true && *routeAdd.Wildcard == true { + return routeAdd, fmt.Errorf("Route %s has wildcard=true and tls-acme=true, this is not supported", routeAdd.Domain) + } + if apiRoute.AlternativeNames != nil && *routeAdd.Wildcard == true { + return routeAdd, fmt.Errorf("Route %s has wildcard=true and alternativenames defined, this is not supported", routeAdd.Domain) + } } - return finalRoutes2 + return routeAdd, nil } diff --git a/internal/lagoon/routes_test.go b/internal/lagoon/routes_test.go index 74db4f2c..d2ff9be5 100644 --- a/internal/lagoon/routes_test.go +++ b/internal/lagoon/routes_test.go @@ -11,23 +11,24 @@ import ( func TestGenerateRouteStructure(t *testing.T) { type args struct { - genRoutes *RoutesV2 - routeMap map[string][]Route + yamlRoutes *RoutesV2 + yamlRouteMap map[string][]Route variables []EnvironmentVariable defaultIngressClass string secretPrefix string activeStandby bool } tests := []struct { - name string - args args - want *RoutesV2 + name string + args args + want *RoutesV2 + wantErr bool }{ { name: "test1", args: args{ - genRoutes: &RoutesV2{}, - routeMap: map[string][]Route{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ "nginx": { { Name: "example.com", @@ -52,6 +53,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -63,6 +65,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, }, }, @@ -70,8 +73,8 @@ func TestGenerateRouteStructure(t *testing.T) { { name: "test2", args: args{ - genRoutes: &RoutesV2{}, - routeMap: map[string][]Route{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ "nginx": { { Name: "example.com", @@ -104,6 +107,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -117,15 +121,64 @@ func TestGenerateRouteStructure(t *testing.T) { Watch: true, ServiceID: "12345", }, + AlternativeNames: []string{}, }, }, }, }, { - name: "test3 - ingress class", + name: "test3", args: args{ - genRoutes: &RoutesV2{}, - routeMap: map[string][]Route{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ + "nginx": { + { + Ingresses: map[string]Ingress{ + "example.com": { + Fastly: Fastly{ + APISecretName: "annotationscom", + Watch: true, + ServiceID: "12345", + }, + AlternativeNames: []string{ + "www.example.com", + "en.example.com", + }, + }, + }, + }, + }, + }, + secretPrefix: "fastly-api-", + activeStandby: false, + }, + want: &RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Fastly: Fastly{ + APISecretName: "fastly-api-annotationscom", + Watch: true, + ServiceID: "12345", + }, + AlternativeNames: []string{ + "www.example.com", + "en.example.com", + }, + }, + }, + }, + }, + { + name: "test4 - ingress class", + args: args{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ "nginx": { { Name: "example.com", @@ -152,6 +205,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -164,15 +218,16 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, }, }, }, { - name: "test4 - custom ingress class on one route", + name: "test5 - custom ingress class on one route", args: args{ - genRoutes: &RoutesV2{}, - routeMap: map[string][]Route{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ "nginx": { { Name: "example.com", @@ -208,6 +263,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -222,15 +278,16 @@ func TestGenerateRouteStructure(t *testing.T) { Watch: true, ServiceID: "12345", }, + AlternativeNames: []string{}, }, }, }, }, { - name: "test5 - hsts", + name: "test6 - hsts", args: args{ - genRoutes: &RoutesV2{}, - routeMap: map[string][]Route{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ "nginx": { { Name: "example.com", @@ -265,6 +322,7 @@ func TestGenerateRouteStructure(t *testing.T) { Fastly: Fastly{ Watch: false, }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -278,8 +336,67 @@ func TestGenerateRouteStructure(t *testing.T) { Watch: true, ServiceID: "12345", }, - HSTSEnabled: helpers.BoolPtr(true), - HSTSMaxAge: 10000, + HSTSEnabled: helpers.BoolPtr(true), + HSTSMaxAge: 10000, + AlternativeNames: []string{}, + }, + }, + }, + }, + { + name: "test7 - wildcard with tls-acme true (should error)", + args: args{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ + "nginx": { + { + Ingresses: map[string]Ingress{ + "www.example.com": { + TLSAcme: helpers.BoolPtr(true), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + secretPrefix: "fastly-api-", + activeStandby: false, + }, + wantErr: true, + want: &RoutesV2{ + Routes: nil, + }, + }, + { + name: "test7 - wildcard with tls-acme false", + args: args{ + yamlRoutes: &RoutesV2{}, + yamlRouteMap: map[string][]Route{ + "nginx": { + { + Ingresses: map[string]Ingress{ + "www.example.com": { + TLSAcme: helpers.BoolPtr(false), + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + }, + secretPrefix: "fastly-api-", + activeStandby: false, + }, + want: &RoutesV2{ + Routes: []RouteV2{ + { + Domain: "www.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + AlternativeNames: []string{}, + Wildcard: helpers.BoolPtr(true), }, }, }, @@ -287,9 +404,13 @@ func TestGenerateRouteStructure(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - GenerateRoutesV2(tt.args.genRoutes, tt.args.routeMap, tt.args.variables, tt.args.defaultIngressClass, tt.args.secretPrefix, tt.args.activeStandby) - if !cmp.Equal(tt.args.genRoutes, tt.want) { - stra, _ := json.Marshal(tt.args.genRoutes) + err := GenerateRoutesV2(tt.args.yamlRoutes, tt.args.yamlRouteMap, tt.args.variables, tt.args.defaultIngressClass, tt.args.secretPrefix, tt.args.activeStandby) + if (err != nil) != tt.wantErr { + t.Errorf("GenerateRouteStructure() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !cmp.Equal(tt.args.yamlRoutes, tt.want) && !tt.wantErr { + stra, _ := json.Marshal(tt.args.yamlRoutes) strb, _ := json.Marshal(tt.want) t.Errorf("GenerateRouteStructure() = %v, want %v", string(stra), string(strb)) } @@ -299,21 +420,22 @@ func TestGenerateRouteStructure(t *testing.T) { func TestMergeRouteStructures(t *testing.T) { type args struct { - genRoutes RoutesV2 + yamlRoutes RoutesV2 apiRoutes RoutesV2 variables []EnvironmentVariable defaultIngressClass string secretPrefix string } tests := []struct { - name string - args args - want RoutesV2 + name string + args args + want RoutesV2 + wantErr bool }{ { name: "test1", args: args{ - genRoutes: RoutesV2{ + yamlRoutes: RoutesV2{ Routes: []RouteV2{ { Domain: "example.com", @@ -396,6 +518,7 @@ func TestMergeRouteStructures(t *testing.T) { ServiceID: "12345", APISecretName: "fastly-api-annotationscom", }, + AlternativeNames: []string{}, }, { Domain: "www.example.com", @@ -406,32 +529,222 @@ func TestMergeRouteStructures(t *testing.T) { Annotations: map[string]string{ "nginx": "nginx", }, + AlternativeNames: []string{}, }, { - Domain: "hsts.example.com", - LagoonService: "nginx", - MonitoringPath: "/", - Insecure: helpers.StrPtr("Redirect"), - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, - HSTSEnabled: helpers.BoolPtr(true), - HSTSMaxAge: 10000, + Domain: "hsts.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + HSTSEnabled: helpers.BoolPtr(true), + HSTSMaxAge: 10000, + AlternativeNames: []string{}, }, { - Domain: "another.example.com", - LagoonService: "nginx", - MonitoringPath: "/", - Insecure: helpers.StrPtr("Redirect"), - TLSAcme: helpers.BoolPtr(true), - Annotations: map[string]string{}, + Domain: "another.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + AlternativeNames: []string{}, + }, + }, + }, + }, + { + name: "test2 - wildcard with tls-acme changed to false", + args: args{ + yamlRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + { + Domain: "a.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + apiRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + { + Domain: "a.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + secretPrefix: "fastly-api-", + }, + want: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + AlternativeNames: []string{}, + Wildcard: helpers.BoolPtr(true), + }, + { + Domain: "a.example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(false), + Annotations: map[string]string{}, + AlternativeNames: []string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + }, + { + name: "test3 - wildcard with tls-acme true (should error)", + args: args{ + yamlRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + apiRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, }, }, + secretPrefix: "fastly-api-", + }, + wantErr: true, + want: RoutesV2{ + Routes: nil, + }, + }, + { + name: "test4 - invalid yaml route", + args: args{ + yamlRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "*._re/f#3safasF*.was_-..asfexample.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + apiRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "fail@example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + secretPrefix: "fastly-api-", + }, + wantErr: true, + want: RoutesV2{ + Routes: nil, + }, + }, + { + name: "test5 - invalid api route", + args: args{ + yamlRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + apiRoutes: RoutesV2{ + Routes: []RouteV2{ + { + Domain: "fail@example.com", + LagoonService: "nginx", + MonitoringPath: "/", + Insecure: helpers.StrPtr("Redirect"), + TLSAcme: helpers.BoolPtr(true), + Annotations: map[string]string{}, + Wildcard: helpers.BoolPtr(true), + }, + }, + }, + secretPrefix: "fastly-api-", + }, + wantErr: true, + want: RoutesV2{ + Routes: nil, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := MergeRoutesV2(tt.args.genRoutes, tt.args.apiRoutes, tt.args.variables, tt.args.defaultIngressClass, tt.args.secretPrefix); !reflect.DeepEqual(got, tt.want) { + got, err := MergeRoutesV2(tt.args.yamlRoutes, tt.args.apiRoutes, tt.args.variables, tt.args.defaultIngressClass, tt.args.secretPrefix) + if (err != nil) != tt.wantErr { + t.Errorf("MergeRouteStructures() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) && !tt.wantErr { stra, _ := json.Marshal(got) strb, _ := json.Marshal(tt.want) t.Errorf("MergeRouteStructures() = %v, want %v", string(stra), string(strb)) diff --git a/internal/templating/ingress/templates_ingress.go b/internal/templating/ingress/templates_ingress.go index 2fee60c9..88bb863b 100644 --- a/internal/templating/ingress/templates_ingress.go +++ b/internal/templating/ingress/templates_ingress.go @@ -25,20 +25,14 @@ func GenerateIngressTemplate( lValues generator.BuildValues, ) ([]byte, error) { - // lowercase any domains then validate them - routeDomain := strings.ToLower(route.Domain) - if err := validation.IsDNS1123Subdomain(strings.ToLower(routeDomain)); err != nil { - return nil, fmt.Errorf("the provided domain name %s is not valid: %v", route.Domain, err) - } - // truncate the route for use in labels and secretname - truncatedRouteDomain := routeDomain + truncatedRouteDomain := route.Domain if len(truncatedRouteDomain) >= 53 { subdomain := strings.Split(truncatedRouteDomain, ".")[0] if errs := utilvalidation.IsValidLabelValue(subdomain); errs != nil { subdomain = subdomain[:53] } - truncatedRouteDomain = fmt.Sprintf("%s-%s", strings.Split(subdomain, ".")[0], helpers.GetMD5HashWithNewLine(routeDomain)[:5]) + truncatedRouteDomain = fmt.Sprintf("%s-%s", strings.Split(subdomain, ".")[0], helpers.GetMD5HashWithNewLine(route.Domain)[:5]) } // create the ingress object for templating @@ -47,11 +41,32 @@ func GenerateIngressTemplate( Kind: "Ingress", APIVersion: "networking.k8s.io/v1", } - ingress.ObjectMeta.Name = routeDomain + ingress.ObjectMeta.Name = route.Domain if route.Autogenerated { // autogenerated routes just have the service name ingress.ObjectMeta.Name = route.LagoonService } + + // if this is a wildcard ingress, handle templating that here + if route.Wildcard != nil && *route.Wildcard == true { + // prefix the object name with wildcard + ingress.ObjectMeta.Name = fmt.Sprintf("wildcard-%s", ingress.ObjectMeta.Name) + // if the new name exceeds the validation spec, truncate it + if err := validation.IsDNS1123Subdomain(strings.ToLower(ingress.ObjectMeta.Name)); err != nil { + ingress.ObjectMeta.Name = fmt.Sprintf("%s-%s", ingress.ObjectMeta.Name[:len(ingress.ObjectMeta.Name)-10], helpers.GetMD5HashWithNewLine(route.Domain)[:5]) + } + truncatedRouteDomain = fmt.Sprintf("wildcard-%s", truncatedRouteDomain) + if len(truncatedRouteDomain) >= 53 { + subdomain := strings.Split(truncatedRouteDomain, "-")[0] + if errs := utilvalidation.IsValidLabelValue(subdomain); errs != nil { + subdomain = subdomain[:53] + } + truncatedRouteDomain = fmt.Sprintf("%s-%s", strings.Split(subdomain, "-")[0], helpers.GetMD5HashWithNewLine(route.Domain)[:5]) + } + // set the domain to include the wildcard prefix + route.Domain = fmt.Sprintf("*.%s", route.Domain) + } + // add the default labels ingress.ObjectMeta.Labels = map[string]string{ "lagoon.sh/autogenerated": "false", @@ -89,7 +104,7 @@ func GenerateIngressTemplate( primaryIngress, _ := url.Parse(lValues.Route) // check if monitoring enabled, route isn't autogenerated, and the primary ingress from the .lagoon.yml is this processed routedomain // and enable monitoring on the primary ingress only. - if lValues.Monitoring.Enabled && !route.Autogenerated && primaryIngress.Host == routeDomain { + if lValues.Monitoring.Enabled && !route.Autogenerated && primaryIngress.Host == route.Domain { additionalLabels["lagoon.sh/primaryIngress"] = "true" // only add the monitring annotations if monitoring is enabled @@ -185,13 +200,13 @@ func GenerateIngressTemplate( // validate any annotations if err := apivalidation.ValidateAnnotations(ingress.ObjectMeta.Annotations, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the annotations for %s are not valid: %v", routeDomain, err) + return nil, fmt.Errorf("the annotations for %s are not valid: %v", route.Domain, err) } } // validate any labels if err := metavalidation.ValidateLabels(ingress.ObjectMeta.Labels, nil); err != nil { if len(err) != 0 { - return nil, fmt.Errorf("the labels for %s are not valid: %v", routeDomain, err) + return nil, fmt.Errorf("the labels for %s are not valid: %v", route.Domain, err) } } @@ -220,13 +235,13 @@ func GenerateIngressTemplate( // use the compose service name to check this, as this is how Services are populated from the compose generation for _, service := range lValues.Services { if service.Name == route.ComposeService { - if service.ShortAutogeneratedRouteDomain != "" && len(routeDomain) > 63 { + if service.ShortAutogeneratedRouteDomain != "" && len(route.Domain) > 63 { ingress.Spec.TLS[0].Hosts = append(ingress.Spec.TLS[0].Hosts, service.ShortAutogeneratedRouteDomain) } } } // add the main domain to the tls spec now - ingress.Spec.TLS[0].Hosts = append(ingress.Spec.TLS[0].Hosts, routeDomain) + ingress.Spec.TLS[0].Hosts = append(ingress.Spec.TLS[0].Hosts, route.Domain) // default service port is http in all lagoon deployments servicePort := networkv1.ServiceBackendPort{ @@ -251,7 +266,7 @@ func GenerateIngressTemplate( // add the main domain as the first rule in the spec ingress.Spec.Rules = []networkv1.IngressRule{ { - Host: routeDomain, + Host: route.Domain, IngressRuleValue: networkv1.IngressRuleValue{ HTTP: &networkv1.HTTPIngressRuleValue{ Paths: []networkv1.HTTPIngressPath{ diff --git a/internal/templating/ingress/templates_ingress_test.go b/internal/templating/ingress/templates_ingress_test.go index 5c290ace..8f2c4cd7 100644 --- a/internal/templating/ingress/templates_ingress_test.go +++ b/internal/templating/ingress/templates_ingress_test.go @@ -257,10 +257,10 @@ func TestGenerateKubeTemplate(t *testing.T) { want: "test-resources/result-custom-ingress5.yaml", }, { - name: "test6 - invalid domain", + name: "test6 - invalid annotation", args: args{ route: lagoon.RouteV2{ - Domain: "fail@.extra-long-name.a-really-long-name-that-should-truncate.www.example.com", + Domain: "extra-long-name.a-really-long-name-that-should-truncate.www.example.com", LagoonService: "nginx", MonitoringPath: "/", Insecure: helpers.StrPtr("Redirect"), @@ -268,6 +268,7 @@ func TestGenerateKubeTemplate(t *testing.T) { Migrate: helpers.BoolPtr(false), Annotations: map[string]string{ "custom-annotation": "custom annotation value", + "@invalid": "this is an invalid annotation", }, Fastly: lagoon.Fastly{ Watch: false, @@ -294,7 +295,7 @@ func TestGenerateKubeTemplate(t *testing.T) { wantErr: true, }, { - name: "test7 - invalid annotation", + name: "test7 - invalid label", args: args{ route: lagoon.RouteV2{ Domain: "extra-long-name.a-really-long-name-that-should-truncate.www.example.com", @@ -305,7 +306,9 @@ func TestGenerateKubeTemplate(t *testing.T) { Migrate: helpers.BoolPtr(false), Annotations: map[string]string{ "custom-annotation": "custom annotation value", - "@invalid": "this is an invalid annotation", + }, + Labels: map[string]string{ + "@invalid": "this is an invalid annotation", }, Fastly: lagoon.Fastly{ Watch: false, @@ -332,10 +335,10 @@ func TestGenerateKubeTemplate(t *testing.T) { wantErr: true, }, { - name: "test8 - invalid label", + name: "test8 - custom ingress with exceptionally long subdomain", args: args{ route: lagoon.RouteV2{ - Domain: "extra-long-name.a-really-long-name-that-should-truncate.www.example.com", + Domain: "hmm-this-is-a-really-long-branch-name-designed-to-test-a-specific-feature.www.example.com", LagoonService: "nginx", MonitoringPath: "/", Insecure: helpers.StrPtr("Redirect"), @@ -344,9 +347,6 @@ func TestGenerateKubeTemplate(t *testing.T) { Annotations: map[string]string{ "custom-annotation": "custom annotation value", }, - Labels: map[string]string{ - "@invalid": "this is an invalid annotation", - }, Fastly: lagoon.Fastly{ Watch: false, }, @@ -369,17 +369,17 @@ func TestGenerateKubeTemplate(t *testing.T) { }, activeStandby: false, }, - wantErr: true, + want: "test-resources/result-custom-ingress6.yaml", }, { - name: "test9 - too long domain", + name: "test9 - wildcard ingress", args: args{ route: lagoon.RouteV2{ - Domain: "extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.www.example.com", + Domain: "www.example.com", LagoonService: "nginx", MonitoringPath: "/", Insecure: helpers.StrPtr("Redirect"), - TLSAcme: helpers.BoolPtr(true), + TLSAcme: helpers.BoolPtr(false), Migrate: helpers.BoolPtr(false), Annotations: map[string]string{ "custom-annotation": "custom annotation value", @@ -388,16 +388,17 @@ func TestGenerateKubeTemplate(t *testing.T) { Watch: false, }, IngressClass: "nginx", + Wildcard: helpers.BoolPtr(true), }, values: generator.BuildValues{ Project: "example-project", - Environment: "environment-with-really-really-reall-3fdb", + Environment: "environment", EnvironmentType: "development", - Namespace: "myexample-project-environment-with-really-really-reall-3fdb", + Namespace: "myexample-project-environment", BuildType: "branch", LagoonVersion: "v2.x.x", Kubernetes: "lagoon.local", - Branch: "environment-with-really-really-reall-3fdb", + Branch: "environment", Monitoring: generator.MonitoringConfig{ AlertContact: "abcdefg", StatusPageID: "12345", @@ -406,17 +407,17 @@ func TestGenerateKubeTemplate(t *testing.T) { }, activeStandby: false, }, - wantErr: true, + want: "test-resources/result-wildcard-ingress1.yaml", }, { - name: "test10 - custom ingress with exceptionally long subdomain", + name: "test10 - wildcard ingress", args: args{ route: lagoon.RouteV2{ - Domain: "hmm-this-is-a-really-long-branch-name-designed-to-test-a-specific-feature.www.example.com", + Domain: "this-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.www.example.com", LagoonService: "nginx", MonitoringPath: "/", Insecure: helpers.StrPtr("Redirect"), - TLSAcme: helpers.BoolPtr(true), + TLSAcme: helpers.BoolPtr(false), Migrate: helpers.BoolPtr(false), Annotations: map[string]string{ "custom-annotation": "custom annotation value", @@ -425,16 +426,17 @@ func TestGenerateKubeTemplate(t *testing.T) { Watch: false, }, IngressClass: "nginx", + Wildcard: helpers.BoolPtr(true), }, values: generator.BuildValues{ Project: "example-project", - Environment: "environment-with-really-really-reall-3fdb", + Environment: "environment", EnvironmentType: "development", - Namespace: "myexample-project-environment-with-really-really-reall-3fdb", + Namespace: "myexample-project-environment", BuildType: "branch", LagoonVersion: "v2.x.x", Kubernetes: "lagoon.local", - Branch: "environment-with-really-really-reall-3fdb", + Branch: "environment", Monitoring: generator.MonitoringConfig{ AlertContact: "abcdefg", StatusPageID: "12345", @@ -443,7 +445,7 @@ func TestGenerateKubeTemplate(t *testing.T) { }, activeStandby: false, }, - want: "test-resources/result-custom-ingress6.yaml", + want: "test-resources/result-wildcard-ingress2.yaml", }, } for _, tt := range tests { diff --git a/internal/templating/ingress/test-resources/result-wildcard-ingress1.yaml b/internal/templating/ingress/test-resources/result-wildcard-ingress1.yaml new file mode 100644 index 00000000..10d657ac --- /dev/null +++ b/internal/templating/ingress/test-resources/result-wildcard-ingress1.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + acme.cert-manager.io/http01-ingress-class: nginx + custom-annotation: custom annotation value + fastly.amazee.io/watch: "false" + ingress.kubernetes.io/ssl-redirect: "true" + kubernetes.io/tls-acme: "false" + lagoon.sh/branch: environment + lagoon.sh/version: v2.x.x + nginx.ingress.kubernetes.io/server-snippet: | + add_header X-Robots-Tag "noindex, nofollow"; + nginx.ingress.kubernetes.io/ssl-redirect: "true" + creationTimestamp: null + labels: + app.kubernetes.io/instance: wildcard-www.example.com + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-ingress + helm.sh/chart: custom-ingress-0.1.0 + lagoon.sh/autogenerated: "false" + lagoon.sh/buildType: branch + lagoon.sh/environment: environment + lagoon.sh/environmentType: development + lagoon.sh/project: example-project + lagoon.sh/service: wildcard-www.example.com + lagoon.sh/service-type: custom-ingress + name: wildcard-www.example.com +spec: + ingressClassName: nginx + rules: + - host: '*.www.example.com' + http: + paths: + - backend: + service: + name: nginx + port: + name: http + path: / + pathType: Prefix + tls: + - hosts: + - '*.www.example.com' + secretName: wildcard-www.example.com-tls +status: + loadBalancer: {} diff --git a/internal/templating/ingress/test-resources/result-wildcard-ingress2.yaml b/internal/templating/ingress/test-resources/result-wildcard-ingress2.yaml new file mode 100644 index 00000000..8dc4e2b3 --- /dev/null +++ b/internal/templating/ingress/test-resources/result-wildcard-ingress2.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + acme.cert-manager.io/http01-ingress-class: nginx + custom-annotation: custom annotation value + fastly.amazee.io/watch: "false" + ingress.kubernetes.io/ssl-redirect: "true" + kubernetes.io/tls-acme: "false" + lagoon.sh/branch: environment + lagoon.sh/version: v2.x.x + nginx.ingress.kubernetes.io/server-snippet: | + add_header X-Robots-Tag "noindex, nofollow"; + nginx.ingress.kubernetes.io/ssl-redirect: "true" + creationTimestamp: null + labels: + app.kubernetes.io/instance: wildcard-this-truncate-f1945 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-ingress + helm.sh/chart: custom-ingress-0.1.0 + lagoon.sh/autogenerated: "false" + lagoon.sh/buildType: branch + lagoon.sh/environment: environment + lagoon.sh/environmentType: development + lagoon.sh/project: example-project + lagoon.sh/service: wildcard-this-truncate-f1945 + lagoon.sh/service-type: custom-ingress + name: wildcard-this-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.www.e-f1945 +spec: + ingressClassName: nginx + rules: + - host: '*.this-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.www.example.com' + http: + paths: + - backend: + service: + name: nginx + port: + name: http + path: / + pathType: Prefix + tls: + - hosts: + - '*.this-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.extra-long-name.a-really-long-name-that-should-truncate.www.example.com' + secretName: wildcard-this-truncate-f1945-tls +status: + loadBalancer: {} diff --git a/test-resources/template-ingress/test11/lagoon.yml b/test-resources/template-ingress/test11/lagoon.yml index ca84a886..84ba6edd 100644 --- a/test-resources/template-ingress/test11/lagoon.yml +++ b/test-resources/template-ingress/test11/lagoon.yml @@ -1,5 +1,5 @@ --- -docker-compose-yaml: ../test-resources/template-autogenerated/test20/docker-compose.yml +docker-compose-yaml: ../test-resources/template-ingress/test11/docker-compose.yml project: content-example-com diff --git a/test-resources/template-ingress/test12/lagoon.yml b/test-resources/template-ingress/test12/lagoon.yml index 9d44d43c..6e937dd6 100644 --- a/test-resources/template-ingress/test12/lagoon.yml +++ b/test-resources/template-ingress/test12/lagoon.yml @@ -1,4 +1,4 @@ -docker-compose-yaml: ../test-resources/template-ingress/test1/docker-compose.yml +docker-compose-yaml: ../test-resources/template-ingress/test12/docker-compose.yml environment_variables: git_sha: "true" diff --git a/test-resources/template-ingress/test21-results/example.com.yaml b/test-resources/template-ingress/test21-results/example.com.yaml new file mode 100644 index 00000000..e143e7f9 --- /dev/null +++ b/test-resources/template-ingress/test21-results/example.com.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + fastly.amazee.io/watch: "false" + ingress.kubernetes.io/ssl-redirect: "true" + kubernetes.io/tls-acme: "true" + lagoon.sh/branch: main + lagoon.sh/version: v2.7.x + monitor.stakater.com/enabled: "true" + monitor.stakater.com/overridePath: / + nginx.ingress.kubernetes.io/ssl-redirect: "true" + uptimerobot.monitor.stakater.com/alert-contacts: alertcontact + uptimerobot.monitor.stakater.com/interval: "60" + uptimerobot.monitor.stakater.com/status-pages: statuspageid + creationTimestamp: null + labels: + app.kubernetes.io/instance: example.com + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-ingress + dioscuri.amazee.io/migrate: "false" + helm.sh/chart: custom-ingress-0.1.0 + lagoon.sh/autogenerated: "false" + lagoon.sh/buildType: branch + lagoon.sh/environment: main + lagoon.sh/environmentType: production + lagoon.sh/primaryIngress: "true" + lagoon.sh/project: example-project + lagoon.sh/service: example.com + lagoon.sh/service-type: custom-ingress + name: example.com +spec: + rules: + - host: example.com + http: + paths: + - backend: + service: + name: node + port: + name: http + path: / + pathType: Prefix + - host: www.example.com + http: + paths: + - backend: + service: + name: node + port: + name: http + path: / + pathType: Prefix + - host: en.example.com + http: + paths: + - backend: + service: + name: node + port: + name: http + path: / + pathType: Prefix + tls: + - hosts: + - example.com + - www.example.com + - en.example.com + secretName: example.com-tls +status: + loadBalancer: {} diff --git a/test-resources/template-ingress/test21/docker-compose.yml b/test-resources/template-ingress/test21/docker-compose.yml new file mode 100644 index 00000000..85386270 --- /dev/null +++ b/test-resources/template-ingress/test21/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + node: + networks: + - amazeeio-network + - default + build: + context: . + dockerfile: node.dockerfile + labels: + lagoon.type: node + volumes: + - .:/app:delegated + environment: + - LAGOON_LOCALDEV_HTTP_PORT=3000 + - LAGOON_ROUTE=http://node.docker.amazee.io + +networks: + amazeeio-network: + external: true \ No newline at end of file diff --git a/test-resources/template-ingress/test21/lagoon.yml b/test-resources/template-ingress/test21/lagoon.yml new file mode 100644 index 00000000..cb50c4cc --- /dev/null +++ b/test-resources/template-ingress/test21/lagoon.yml @@ -0,0 +1,13 @@ +docker-compose-yaml: ../test-resources/template-ingress/test21/docker-compose.yml + +environment_variables: + git_sha: "true" + +environments: + main: + routes: + - node: + - example.com: + alternativenames: + - www.example.com + - en.example.com diff --git a/test-resources/template-ingress/test22-results/example.com.yaml b/test-resources/template-ingress/test22-results/example.com.yaml new file mode 100644 index 00000000..ce022329 --- /dev/null +++ b/test-resources/template-ingress/test22-results/example.com.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + fastly.amazee.io/watch: "false" + ingress.kubernetes.io/ssl-redirect: "true" + kubernetes.io/tls-acme: "false" + lagoon.sh/branch: main + lagoon.sh/version: v2.7.x + monitor.stakater.com/enabled: "false" + monitor.stakater.com/overridePath: / + nginx.ingress.kubernetes.io/ssl-redirect: "true" + creationTimestamp: null + labels: + app.kubernetes.io/instance: wildcard-example.com + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-ingress + dioscuri.amazee.io/migrate: "false" + helm.sh/chart: custom-ingress-0.1.0 + lagoon.sh/autogenerated: "false" + lagoon.sh/buildType: branch + lagoon.sh/environment: main + lagoon.sh/environmentType: production + lagoon.sh/project: example-project + lagoon.sh/service: wildcard-example.com + lagoon.sh/service-type: custom-ingress + name: wildcard-example.com +spec: + rules: + - host: '*.example.com' + http: + paths: + - backend: + service: + name: node + port: + name: http + path: / + pathType: Prefix + tls: + - hosts: + - '*.example.com' + secretName: wildcard-example.com-tls +status: + loadBalancer: {} diff --git a/test-resources/template-ingress/test22/docker-compose.yml b/test-resources/template-ingress/test22/docker-compose.yml new file mode 100644 index 00000000..85386270 --- /dev/null +++ b/test-resources/template-ingress/test22/docker-compose.yml @@ -0,0 +1,20 @@ +version: '2' +services: + node: + networks: + - amazeeio-network + - default + build: + context: . + dockerfile: node.dockerfile + labels: + lagoon.type: node + volumes: + - .:/app:delegated + environment: + - LAGOON_LOCALDEV_HTTP_PORT=3000 + - LAGOON_ROUTE=http://node.docker.amazee.io + +networks: + amazeeio-network: + external: true \ No newline at end of file diff --git a/test-resources/template-ingress/test22/lagoon.yml b/test-resources/template-ingress/test22/lagoon.yml new file mode 100644 index 00000000..9f44e4c6 --- /dev/null +++ b/test-resources/template-ingress/test22/lagoon.yml @@ -0,0 +1,12 @@ +docker-compose-yaml: ../test-resources/template-ingress/test22/docker-compose.yml + +environment_variables: + git_sha: "true" + +environments: + main: + routes: + - node: + - example.com: + tls-acme: false + wildcard: true