From 172df457083740c32320fc43f0a4da8f916db600 Mon Sep 17 00:00:00 2001 From: LorcanMcVeigh Date: Thu, 25 Feb 2021 11:12:11 +0000 Subject: [PATCH] Add snippet feature to TransportServer --- cmd/nginx-ingress/main.go | 2 +- .../k8s.nginx.org_transportservers.yaml | 2 + .../crds/k8s.nginx.org_transportservers.yaml | 2 + .../crds/k8s.nginx.org_transportservers.yaml | 2 + .../configuration/transportserver-resource.md | 36 ++++++ internal/configs/configurator.go | 3 +- internal/configs/transportserver.go | 13 ++- internal/configs/transportserver_test.go | 108 +++++++++++++++--- .../version2/nginx-plus.transportserver.tmpl | 4 + .../version2/nginx.transportserver.tmpl | 4 + internal/configs/version2/stream.go | 1 + internal/configs/virtualserver.go | 10 +- internal/configs/virtualserver_test.go | 14 +-- internal/k8s/configuration_test.go | 3 +- pkg/apis/configuration/v1alpha1/types.go | 1 + .../validation/transportserver.go | 19 ++- .../validation/transportserver_test.go | 37 ++++++ 17 files changed, 222 insertions(+), 39 deletions(-) diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index 4df3226a0e..bc2f287d9c 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -604,7 +604,7 @@ func main() { templateExecutorV2, *nginxPlus, isWildcardEnabled, plusCollector, *enablePrometheusMetrics, latencyCollector, *enableLatencyMetrics) controllerNamespace := os.Getenv("POD_NAMESPACE") - transportServerValidator := cr_validation.NewTransportServerValidator(*enableTLSPassthrough) + transportServerValidator := cr_validation.NewTransportServerValidator(*enableTLSPassthrough, *enableSnippets) virtualServerValidator := cr_validation.NewVirtualServerValidator(*nginxPlus) lbcInput := k8s.NewLoadBalancerControllerInput{ diff --git a/deployments/common/crds-v1beta1/k8s.nginx.org_transportservers.yaml b/deployments/common/crds-v1beta1/k8s.nginx.org_transportservers.yaml index 39ea4dbe85..38271d3cca 100644 --- a/deployments/common/crds-v1beta1/k8s.nginx.org_transportservers.yaml +++ b/deployments/common/crds-v1beta1/k8s.nginx.org_transportservers.yaml @@ -64,6 +64,8 @@ spec: type: string protocol: type: string + serverSnippets: + type: string sessionParameters: description: SessionParameters defines session parameters. type: object diff --git a/deployments/common/crds/k8s.nginx.org_transportservers.yaml b/deployments/common/crds/k8s.nginx.org_transportservers.yaml index 16bedd760b..6952e535c7 100644 --- a/deployments/common/crds/k8s.nginx.org_transportservers.yaml +++ b/deployments/common/crds/k8s.nginx.org_transportservers.yaml @@ -63,6 +63,8 @@ spec: type: string protocol: type: string + serverSnippets: + type: string sessionParameters: description: SessionParameters defines session parameters. type: object diff --git a/deployments/helm-chart/crds/k8s.nginx.org_transportservers.yaml b/deployments/helm-chart/crds/k8s.nginx.org_transportservers.yaml index 16bedd760b..6952e535c7 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_transportservers.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_transportservers.yaml @@ -63,6 +63,8 @@ spec: type: string protocol: type: string + serverSnippets: + type: string sessionParameters: description: SessionParameters defines session parameters. type: object diff --git a/docs-web/configuration/transportserver-resource.md b/docs-web/configuration/transportserver-resource.md index 1710644507..840fd49349 100644 --- a/docs-web/configuration/transportserver-resource.md +++ b/docs-web/configuration/transportserver-resource.md @@ -19,6 +19,7 @@ This document is the reference documentation for the TransportServer resource. T - [SessionParameters](#sessionparameters) - [Action](#action) - [Using TransportServer](#using-transportserver) + - [Usings Snippets](#using-snippets) - [Validation](#validation) - [Structural Validation](#structural-validation) - [Comprehensive Validation](#comprehensive-validation) @@ -370,6 +371,41 @@ secure-app 46sm In the kubectl get and similar commands, you can also use the short name `ts` instead of `transportserver`. +### Using Snippets + +Snippets allow you to insert raw NGINX config into different contexts of NGINX configuration. In the example below, we use snippets to configure [access control](http://nginx.org/en/docs/stream/ngx_stream_access_module.html) in a TransportServer: + +```yaml +apiVersion: k8s.nginx.org/v1alpha1 +kind: TransportServer +metadata: + name: cafe +spec: + host: cafe.example.com + serverSnippets: | + deny 192.168.1.1; + allow 192.168.1.0/24; + upstreams: + - name: tea + service: tea-svc + port: 80 +``` + +Snippets are intended to be used by advanced NGINX users who need more control over the generated NGINX configuration. + +However, because of the disadvantages described below, snippets are disabled by default. To use snippets, set the [`enable-snippets`](/nginx-ingress-controller/configuration/global-configuration/command-line-arguments#cmdoption-enable-snippets) command-line argument. + +Disadvantages of using snippets: +* *Complexity*. To use snippets, you will need to: + * Understand NGINX configuration primitives and implement a correct NGINX configuration. + * Understand how the IC generates NGINX configuration so that a snippet doesn't interfere with the other features in the configuration. +* *Decreased robustness*. An incorrect snippet makes the NGINX config invalid which will lead to a failed reload. This will prevent any new configuration updates, including updates for the other TransportServer resource until the snippet is fixed. +* *Security implications*. Snippets give access to NGINX configuration primitives and those primitives are not validated by the Ingress Controller. + + +> Note that during a period when the NGINX config includes an invalid snippet, NGINX will continue to operate with the latest valid configuration. + + ### Validation Two types of validation are available for the TransportServer resource: diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index ff4ceba418..3153dafec2 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -559,7 +559,7 @@ func (cnf *Configurator) addOrUpdateTransportServer(transportServerEx *Transport tsCfg := generateTransportServerConfig(transportServerEx, transportServerEx.ListenerPort, cnf.isPlus) - content, err := cnf.templateExecutorV2.ExecuteTransportServerTemplate(&tsCfg) + content, err := cnf.templateExecutorV2.ExecuteTransportServerTemplate(tsCfg) if err != nil { return fmt.Errorf("Error generating TransportServer config %v: %v", name, err) } @@ -1073,6 +1073,7 @@ func (cnf *Configurator) UpdateConfig(cfgParams *ConfigParams, ingExes []*Ingres return allWarnings, nil } +// UpdateTransportServers updates TransportServers. func (cnf *Configurator) UpdateTransportServers(updatedTSExes []*TransportServerEx, deletedKeys []string) error { for _, tsEx := range updatedTSExes { err := cnf.addOrUpdateTransportServer(tsEx) diff --git a/internal/configs/transportserver.go b/internal/configs/transportserver.go index 62312034bf..b01b13b159 100644 --- a/internal/configs/transportserver.go +++ b/internal/configs/transportserver.go @@ -30,7 +30,7 @@ func (tsEx *TransportServerEx) String() string { } // generateTransportServerConfig generates a full configuration for a TransportServer. -func generateTransportServerConfig(transportServerEx *TransportServerEx, listenerPort int, isPlus bool) version2.TransportServerConfig { +func generateTransportServerConfig(transportServerEx *TransportServerEx, listenerPort int, isPlus bool) *version2.TransportServerConfig { upstreamNamer := newUpstreamNamerForTransportServer(transportServerEx.TransportServer) upstreams := generateStreamUpstreams(transportServerEx, upstreamNamer, isPlus) @@ -59,14 +59,14 @@ func generateTransportServerConfig(transportServerEx *TransportServerEx, listene proxyTimeout = transportServerEx.TransportServer.Spec.SessionParameters.Timeout } - statusZone := "" + serverSnippets := generateSnippets(true, transportServerEx.TransportServer.Spec.ServerSnippets, []string{}) + + statusZone := transportServerEx.TransportServer.Spec.Listener.Name if transportServerEx.TransportServer.Spec.Listener.Name == conf_v1alpha1.TLSPassthroughListenerName { statusZone = transportServerEx.TransportServer.Spec.Host - } else { - statusZone = transportServerEx.TransportServer.Spec.Listener.Name } - return version2.TransportServerConfig{ + tsConfig := &version2.TransportServerConfig{ Server: version2.StreamServer{ TLSPassthrough: transportServerEx.TransportServer.Spec.Listener.Name == conf_v1alpha1.TLSPassthroughListenerName, UnixSocket: generateUnixSocket(transportServerEx), @@ -84,9 +84,12 @@ func generateTransportServerConfig(transportServerEx *TransportServerEx, listene ProxyNextUpstreamTimeout: generateTimeWithDefault(nextUpstreamTimeout, "0s"), ProxyNextUpstreamTries: nextUpstreamTries, HealthCheck: healthCheck, + Snippets: serverSnippets, }, Upstreams: upstreams, } + + return tsConfig } func generateUnixSocket(transportServerEx *TransportServerEx) string { diff --git a/internal/configs/transportserver_test.go b/internal/configs/transportserver_test.go index fa65ccdf53..3ea91322f5 100644 --- a/internal/configs/transportserver_test.go +++ b/internal/configs/transportserver_test.go @@ -1,7 +1,6 @@ package configs import ( - "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -62,6 +61,82 @@ func TestTransportServerExString(t *testing.T) { } } +func TestGenerateTransportServerConfigForTCPSnippets(t *testing.T) { + transportServerEx := TransportServerEx{ + TransportServer: &conf_v1alpha1.TransportServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "tcp-server", + Namespace: "default", + }, + Spec: conf_v1alpha1.TransportServerSpec{ + Listener: conf_v1alpha1.TransportServerListener{ + Name: "tcp-listener", + Protocol: "TCP", + }, + Upstreams: []conf_v1alpha1.Upstream{ + { + Name: "tcp-app", + Service: "tcp-app-svc", + Port: 5001, + }, + }, + Action: &conf_v1alpha1.Action{ + Pass: "tcp-app", + }, + ServerSnippets: "deny 192.168.1.1;\nallow 192.168.1.0/24;", + }, + }, + Endpoints: map[string][]string{ + "default/tcp-app-svc:5001": { + "10.0.0.20:5001", + }, + }, + } + + listenerPort := 2020 + + expected := &version2.TransportServerConfig{ + Upstreams: []version2.StreamUpstream{ + { + Name: "ts_default_tcp-server_tcp-app", + Servers: []version2.StreamUpstreamServer{ + { + Address: "10.0.0.20:5001", + MaxFails: 1, + FailTimeout: "10s", + }, + }, + UpstreamLabels: version2.UpstreamLabels{ + ResourceName: "tcp-server", + ResourceType: "transportserver", + ResourceNamespace: "default", + Service: "tcp-app-svc", + }, + }, + }, + Server: version2.StreamServer{ + Port: listenerPort, + UDP: false, + StatusZone: "tcp-listener", + ProxyPass: "ts_default_tcp-server_tcp-app", + Name: "tcp-server", + Namespace: "default", + ProxyConnectTimeout: "60s", + ProxyNextUpstream: false, + ProxyNextUpstreamTries: 0, + ProxyNextUpstreamTimeout: "0s", + ProxyTimeout: "10m", + HealthCheck: nil, + Snippets: []string{"deny 192.168.1.1;", "allow 192.168.1.0/24;"}, + }, + } + + result := generateTransportServerConfig(&transportServerEx, listenerPort, true) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateTransportServerConfig() mismatch (-want +got):\n%s", diff) + } +} + func TestGenerateTransportServerConfigForTCP(t *testing.T) { transportServerEx := TransportServerEx{ TransportServer: &conf_v1alpha1.TransportServer{ @@ -104,7 +179,7 @@ func TestGenerateTransportServerConfigForTCP(t *testing.T) { listenerPort := 2020 - expected := version2.TransportServerConfig{ + expected := &version2.TransportServerConfig{ Upstreams: []version2.StreamUpstream{ { Name: "ts_default_tcp-server_tcp-app", @@ -136,14 +211,15 @@ func TestGenerateTransportServerConfigForTCP(t *testing.T) { ProxyNextUpstreamTimeout: "0s", ProxyTimeout: "50s", HealthCheck: nil, + Snippets: []string{}, }, } - isPlus := true - result := generateTransportServerConfig(&transportServerEx, listenerPort, isPlus) - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateTransportServerConfig() returned \n%+v but expected \n%+v", result, expected) + result := generateTransportServerConfig(&transportServerEx, listenerPort, true) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateTransportServerConfig() mismatch (-want +got):\n%s", diff) } + } func TestGenerateTransportServerConfigForTLSPasstrhough(t *testing.T) { @@ -186,7 +262,7 @@ func TestGenerateTransportServerConfigForTLSPasstrhough(t *testing.T) { listenerPort := 2020 - expected := version2.TransportServerConfig{ + expected := &version2.TransportServerConfig{ Upstreams: []version2.StreamUpstream{ { Name: "ts_default_tcp-server_tcp-app", @@ -220,13 +296,13 @@ func TestGenerateTransportServerConfigForTLSPasstrhough(t *testing.T) { ProxyNextUpstreamTries: 0, ProxyTimeout: "10m", HealthCheck: nil, + Snippets: []string{}, }, } - isPlus := true - result := generateTransportServerConfig(&transportServerEx, listenerPort, isPlus) - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateTransportServerConfig() returned \n%+v but expected \n%+v", result, expected) + result := generateTransportServerConfig(&transportServerEx, listenerPort, true) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateTransportServerConfig() mismatch (-want +got):\n%s", diff) } } @@ -275,7 +351,7 @@ func TestGenerateTransportServerConfigForUDP(t *testing.T) { listenerPort := 2020 - expected := version2.TransportServerConfig{ + expected := &version2.TransportServerConfig{ Upstreams: []version2.StreamUpstream{ { Name: "ts_default_udp-server_udp-app", @@ -309,13 +385,13 @@ func TestGenerateTransportServerConfigForUDP(t *testing.T) { ProxyNextUpstreamTries: 0, ProxyTimeout: "10m", HealthCheck: nil, + Snippets: []string{}, }, } - isPlus := true - result := generateTransportServerConfig(&transportServerEx, listenerPort, isPlus) - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateTransportServerConfig() returned \n%+v but expected \n%+v", result, expected) + result := generateTransportServerConfig(&transportServerEx, listenerPort, true) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("generateTransportServerConfig() mismatch (-want +got):\n%s", diff) } } diff --git a/internal/configs/version2/nginx-plus.transportserver.tmpl b/internal/configs/version2/nginx-plus.transportserver.tmpl index 1c593f3d23..1bed8a9d55 100644 --- a/internal/configs/version2/nginx-plus.transportserver.tmpl +++ b/internal/configs/version2/nginx-plus.transportserver.tmpl @@ -28,6 +28,10 @@ server { proxy_responses {{ $s.ProxyResponses }}; {{ end }} + {{ range $snippet := $s.Snippets }} + {{- $snippet }} + {{ end }} + proxy_pass {{ $s.ProxyPass }}; {{ if $s.HealthCheck }} diff --git a/internal/configs/version2/nginx.transportserver.tmpl b/internal/configs/version2/nginx.transportserver.tmpl index 65caabda1e..014f5ee2a2 100644 --- a/internal/configs/version2/nginx.transportserver.tmpl +++ b/internal/configs/version2/nginx.transportserver.tmpl @@ -26,6 +26,10 @@ server { proxy_responses {{ $s.ProxyResponses }}; {{ end }} + {{ range $snippet := $s.Snippets }} + {{- $snippet }} + {{ end }} + proxy_pass {{ $s.ProxyPass }}; proxy_timeout {{ $s.ProxyTimeout }}; diff --git a/internal/configs/version2/stream.go b/internal/configs/version2/stream.go index da065791c5..4ef2ceaeed 100644 --- a/internal/configs/version2/stream.go +++ b/internal/configs/version2/stream.go @@ -38,6 +38,7 @@ type StreamServer struct { ProxyNextUpstreamTimeout string ProxyNextUpstreamTries int HealthCheck *StreamHealthCheck + Snippets []string } // StreamHealthCheck defines a health check for a StreamUpstream in a StreamServer. diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 469dcf9e6a..de49108aa0 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -557,7 +557,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS } } - httpSnippets := generateSnippets(vsc.enableSnippets, vsEx.VirtualServer.Spec.HTTPSnippets, []string{""}) + httpSnippets := generateSnippets(vsc.enableSnippets, vsEx.VirtualServer.Spec.HTTPSnippets, []string{}) serverSnippets := generateSnippets( vsc.enableSnippets, vsEx.VirtualServer.Spec.ServerSnippets, @@ -1410,11 +1410,11 @@ func generateTimeWithDefault(value string, defaultValue string) string { return generateTime(value) } -func generateSnippets(enableSnippets bool, s string, defaultS []string) []string { - if !enableSnippets || s == "" { - return defaultS +func generateSnippets(enableSnippets bool, snippet string, defaultSnippets []string) []string { + if !enableSnippets || snippet == "" { + return defaultSnippets } - return strings.Split(s, "\n") + return strings.Split(snippet, "\n") } func generateBuffers(s *conf_v1.UpstreamBuffers, defaultS string) string { diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index dc0f673989..3c939cc0df 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -482,7 +482,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { Keepalive: 16, }, }, - HTTPSnippets: []string{""}, + HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", @@ -705,7 +705,7 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { Keepalive: 16, }, }, - HTTPSnippets: []string{""}, + HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", @@ -946,7 +946,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { }, }, }, - HTTPSnippets: []string{""}, + HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", @@ -1259,7 +1259,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { }, }, }, - HTTPSnippets: []string{""}, + HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", @@ -1572,7 +1572,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { }, }, }, - HTTPSnippets: []string{""}, + HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "example.com", @@ -3682,7 +3682,7 @@ func TestGenerateSnippets(t *testing.T) { { true, "test", - []string{""}, + []string{}, []string{"test"}, }, { @@ -3694,7 +3694,7 @@ func TestGenerateSnippets(t *testing.T) { { true, "test\none\ntwo", - []string{""}, + []string{}, []string{"test", "one", "two"}, }, { diff --git a/internal/k8s/configuration_test.go b/internal/k8s/configuration_test.go index e7600bc88f..9927a37e45 100644 --- a/internal/k8s/configuration_test.go +++ b/internal/k8s/configuration_test.go @@ -22,6 +22,7 @@ func createTestConfiguration() *Configuration { appProtectEnabled := false internalRoutesEnabled := false isTLSPassthroughEnabled := true + snippetsEnabled := true return NewConfiguration( lbc.HasCorrectIngressClass, isPlus, @@ -32,7 +33,7 @@ func createTestConfiguration() *Configuration { 80: true, 443: true, }), - validation.NewTransportServerValidator(isTLSPassthroughEnabled), + validation.NewTransportServerValidator(isTLSPassthroughEnabled, snippetsEnabled), isTLSPassthroughEnabled, ) } diff --git a/pkg/apis/configuration/v1alpha1/types.go b/pkg/apis/configuration/v1alpha1/types.go index be0b2b452d..beef758c22 100644 --- a/pkg/apis/configuration/v1alpha1/types.go +++ b/pkg/apis/configuration/v1alpha1/types.go @@ -68,6 +68,7 @@ type TransportServer struct { type TransportServerSpec struct { IngressClass string `json:"ingressClassName"` Listener TransportServerListener `json:"listener"` + ServerSnippets string `json:"serverSnippets"` Host string `json:"host"` Upstreams []Upstream `json:"upstreams"` UpstreamParameters *UpstreamParameters `json:"upstreamParameters"` diff --git a/pkg/apis/configuration/validation/transportserver.go b/pkg/apis/configuration/validation/transportserver.go index 4f2e324a65..55ba51be07 100644 --- a/pkg/apis/configuration/validation/transportserver.go +++ b/pkg/apis/configuration/validation/transportserver.go @@ -11,13 +11,15 @@ import ( // TransportServerValidator validates a TransportServer resource. type TransportServerValidator struct { - tlsPassthrough bool + tlsPassthrough bool + snippetsEnabled bool } // NewTransportServerValidator creates a new TransportServerValidator. -func NewTransportServerValidator(tlsPassthrough bool) *TransportServerValidator { +func NewTransportServerValidator(tlsPassthrough bool, snippetsEnabled bool) *TransportServerValidator { return &TransportServerValidator{ - tlsPassthrough: tlsPassthrough, + tlsPassthrough: tlsPassthrough, + snippetsEnabled: snippetsEnabled, } } @@ -48,6 +50,17 @@ func (tsv *TransportServerValidator) validateTransportServerSpec(spec *v1alpha1. allErrs = append(allErrs, validateTransportServerAction(spec.Action, fieldPath.Child("action"), upstreamNames)...) } + allErrs = append(allErrs, validateSnippets(spec.ServerSnippets, fieldPath.Child("serverSnippets"), tsv.snippetsEnabled)...) + + return allErrs +} + +func validateSnippets(serverSnippet string, fieldPath *field.Path, snippetsEnabled bool) field.ErrorList { + allErrs := field.ErrorList{} + if !snippetsEnabled && serverSnippet != "" { + return append(allErrs, field.Forbidden(fieldPath, "snippet specified but snippets feature is not enabled")) + } + return allErrs } diff --git a/pkg/apis/configuration/validation/transportserver_test.go b/pkg/apis/configuration/validation/transportserver_test.go index 36231b9bac..75be6769c8 100644 --- a/pkg/apis/configuration/validation/transportserver_test.go +++ b/pkg/apis/configuration/validation/transportserver_test.go @@ -206,6 +206,43 @@ func TestValidateTransportServerHost(t *testing.T) { } } +func TestValidateTransportServerSnippet(t *testing.T) { + tests := []struct { + snippet string + isSnippetsEnabled bool + expectError bool + }{ + { + snippet: "", + isSnippetsEnabled: false, + expectError: false, + }, + { + snippet: "deny 192.168.1.1;", + isSnippetsEnabled: false, + expectError: true, + }, + { + snippet: "deny 192.168.1.1;", + isSnippetsEnabled: true, + expectError: false, + }, + } + + for _, test := range tests { + allErrs := validateSnippets(test.snippet, field.NewPath("serverSnippet"), test.isSnippetsEnabled) + if test.expectError { + if len(allErrs) < 1 { + t.Errorf("validateSnippets(%q, %v) failed to return an error for invalid input", test.snippet, test.isSnippetsEnabled) + } + } else { + if len(allErrs) > 0 { + t.Errorf("validateSnippets(%q, %v) returned errors %v for valid input", test.snippet, test.isSnippetsEnabled, allErrs) + } + } + } +} + func TestValidateTransportServerHostFails(t *testing.T) { tests := []struct { host string