diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml new file mode 100644 index 000000000..f99bde07c --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_deep_read_secret.yml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Secret +metadata: + name: name + namespace: default + +data: + key: dGhpc19pc190aGVfdmVyeV9kZWVwX3NlY3JldA== + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-secret + namespace: default + +spec: + plugin: + test-secret: + secret_0: + secret_1: + secret_2: + user: admin + secret: urn:k8s:secret:name:key diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml new file mode 100644 index 000000000..36042fe57 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_map_contain_secret.yml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Secret +metadata: + name: name + namespace: default + +data: + key1: YWRtaW5fcGFzc3dvcmQ= + key2: dXNlcl9wYXNzd29yZA== + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-secret + namespace: default + +spec: + plugin: + test-secret: + users: + - name: admin + secret: urn:k8s:secret:name:key1 + - name: user + secret: urn:k8s:secret:name:key2 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml new file mode 100644 index 000000000..7b88c1c42 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_array_of_secret.yml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: name + namespace: default + +data: + key1: c2VjcmV0X2RhdGEx + key2: c2VjcmV0X2RhdGEy + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-secret + namespace: default + +spec: + plugin: + test-secret: + secret: + - urn:k8s:secret:name:key1 + - urn:k8s:secret:name:key2 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml new file mode 100644 index 000000000..c9923d1d8 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_not_exist_secret.yml @@ -0,0 +1,11 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-secret + namespace: default + +spec: + plugin: + test-secret: + user: admin + secret: urn:k8s:secret:notfound:key diff --git a/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml new file mode 100644 index 000000000..bcd368619 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_plugin_read_secret.yml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Secret +metadata: + name: name + namespace: default + +data: + key: dGhpc19pc190aGVfc2VjcmV0 + +--- +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: test-secret + namespace: default + +spec: + plugin: + test-secret: + user: admin + secret: urn:k8s:secret:name:key diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7270c37d8..bfc9d3fa3 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -232,7 +232,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) conf.HTTP.Services[serviceName] = errorPageService } - plugin, err := createPluginMiddleware(middleware.Spec.Plugin) + plugin, err := createPluginMiddleware(client, middleware.Namespace, middleware.Spec.Plugin) if err != nil { log.FromContext(ctxMid).Errorf("Error while reading plugins middleware: %v", err) continue @@ -419,7 +419,7 @@ func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.Servi return &corev1.ServicePort{Port: port.IntVal}, nil } -func createPluginMiddleware(plugins map[string]apiextensionv1.JSON) (map[string]dynamic.PluginConf, error) { +func createPluginMiddleware(k8sClient Client, ns string, plugins map[string]apiextensionv1.JSON) (map[string]dynamic.PluginConf, error) { if plugins == nil { return nil, nil } @@ -429,13 +429,73 @@ func createPluginMiddleware(plugins map[string]apiextensionv1.JSON) (map[string] return nil, err } - pc := map[string]dynamic.PluginConf{} - err = json.Unmarshal(data, &pc) - if err != nil { + pcMap := map[string]dynamic.PluginConf{} + if err = json.Unmarshal(data, &pcMap); err != nil { return nil, err } - return pc, nil + for _, pc := range pcMap { + for key := range pc { + if pc[key], err = loadSecretKeys(k8sClient, ns, pc[key]); err != nil { + return nil, err + } + } + } + + return pcMap, nil +} + +func loadSecretKeys(k8sClient Client, ns string, i interface{}) (interface{}, error) { + var err error + switch iv := i.(type) { + case string: + if !strings.HasPrefix(iv, "urn:k8s:secret:") { + return iv, nil + } + + return getSecretValue(k8sClient, ns, iv) + + case []interface{}: + for i := range iv { + if iv[i], err = loadSecretKeys(k8sClient, ns, iv[i]); err != nil { + return nil, err + } + } + + case map[string]interface{}: + for k := range iv { + if iv[k], err = loadSecretKeys(k8sClient, ns, iv[k]); err != nil { + return nil, err + } + } + } + + return i, nil +} + +func getSecretValue(c Client, ns, urn string) (string, error) { + parts := strings.Split(urn, ":") + if len(parts) != 5 { + return "", fmt.Errorf("malformed secret URN %q", urn) + } + + secretName := parts[3] + secret, ok, err := c.GetSecret(ns, secretName) + if err != nil { + return "", err + } + + if !ok { + return "", fmt.Errorf("secret %s/%s is not found", ns, secretName) + } + + secretKey := parts[4] + secretValue, ok := secret.Data[secretKey] + if !ok { + return "", fmt.Errorf("key %q not found in secret %s/%s", secretKey, ns, secretName) + } + + return string(secretValue), nil } func createCircuitBreakerMiddleware(circuitBreaker *v1alpha1.CircuitBreaker) (*dynamic.CircuitBreaker, error) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 6c081f0b1..e1c094403 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3339,6 +3339,166 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "Simple Ingress Route, with test middleware read config from secret", + paths: []string{"services.yml", "with_plugin_read_secret.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default-test-secret": { + Plugin: map[string]dynamic.PluginConf{ + "test-secret": map[string]interface{}{ + "user": "admin", + "secret": "this_is_the_secret", + }, + }, + }, + }, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "Simple Ingress Route, with test middleware read config from deep secret", + paths: []string{"services.yml", "with_plugin_deep_read_secret.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default-test-secret": { + Plugin: map[string]dynamic.PluginConf{ + "test-secret": map[string]interface{}{ + "secret_0": map[string]interface{}{ + "secret_1": map[string]interface{}{ + "secret_2": map[string]interface{}{ + "user": "admin", + "secret": "this_is_the_very_deep_secret", + }, + }, + }, + }, + }, + }, + }, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "Simple Ingress Route, with test middleware read config from an array of secret", + paths: []string{"services.yml", "with_plugin_read_array_of_secret.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default-test-secret": { + Plugin: map[string]dynamic.PluginConf{ + "test-secret": map[string]interface{}{ + "secret": []interface{}{"secret_data1", "secret_data2"}, + }, + }, + }, + }, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "Simple Ingress Route, with test middleware read config from an array of secret", + paths: []string{"services.yml", "with_plugin_read_array_of_map_contain_secret.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default-test-secret": { + Plugin: map[string]dynamic.PluginConf{ + "test-secret": map[string]interface{}{ + "users": []interface{}{ + map[string]interface{}{ + "name": "admin", + "secret": "admin_password", + }, + map[string]interface{}{ + "name": "user", + "secret": "user_password", + }, + }, + }, + }, + }, + }, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, + { + desc: "Simple Ingress Route, with test middleware read config from secret that not found", + paths: []string{"services.yml", "with_plugin_read_not_exist_secret.yml"}, + allowCrossNamespace: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + }, + }, { desc: "Simple Ingress Route, with error page middleware", paths: []string{"services.yml", "with_error_page.yml"},