/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package egressselector

import (
	"fmt"
	"os"
	"reflect"
	"strings"
	"testing"

	utiltesting "k8s.io/client-go/util/testing"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apiserver/pkg/apis/apiserver"
	"k8s.io/utils/ptr"
)

func TestReadEgressSelectorConfiguration(t *testing.T) {
	testcases := []struct {
		name           string
		contents       string
		createFile     bool
		expectedResult *apiserver.EgressSelectorConfiguration
		expectedError  *string
	}{
		{
			name:           "empty",
			createFile:     true,
			contents:       ``,
			expectedResult: nil,
			expectedError:  ptr.To("invalid service configuration object \"\""),
		},
		{
			name:           "absent",
			createFile:     false,
			contents:       ``,
			expectedResult: nil,
			expectedError:  ptr.To("errors.errorString{s:\"unable to read egress selector configuration"),
		},
		{
			name:       "unknown field causes error",
			createFile: false,
			contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "etcd"
  connection:
    proxyProtocol: "Direct"
  foo:
    bar: "baz"
`,
			expectedResult: nil,
			expectedError:  ptr.To("runtime.strictDecodingError"),
		},
		{
			name:       "duplicate field causes error",
			createFile: false,
			contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "etcd"
  connection:
    proxyProtocol: "Direct"
  connection:
    proxyProtocol: "Indirect"
`,
			expectedResult: nil,
			expectedError:  ptr.To("runtime.strictDecodingError"),
		},
		{
			name:       "v1beta1",
			createFile: true,
			contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "cluster"
  connection:
    proxyProtocol: "HTTPConnect"
    transport:
      tcp:
        url: "https://127.0.0.1:8131"
        tlsConfig:
          caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt"
          clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key"
          clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt"
- name: "controlplane"
  connection:
    proxyProtocol: "HTTPConnect"
    transport:
      tcp:
        url: "https://127.0.0.1:8132"
        tlsConfig:
          caBundle: "/etc/srv/kubernetes/pki/konnectivity-server-master/ca.crt"
          clientKey: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.key"
          clientCert: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.crt"
- name: "etcd"
  connection:
    proxyProtocol: "Direct"
`,
			expectedResult: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: "HTTPConnect",
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://127.0.0.1:8131",

									TLSConfig: &apiserver.TLSConfig{
										CABundle:   "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt",
										ClientKey:  "/etc/srv/kubernetes/pki/konnectivity-server/client.key",
										ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt",
									},
								},
							},
						},
					},
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: "HTTPConnect",
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://127.0.0.1:8132",
									TLSConfig: &apiserver.TLSConfig{
										CABundle:   "/etc/srv/kubernetes/pki/konnectivity-server-master/ca.crt",
										ClientKey:  "/etc/srv/kubernetes/pki/konnectivity-server-master/client.key",
										ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.crt",
									},
								},
							},
						},
					},
					{
						Name: "etcd",
						Connection: apiserver.Connection{
							ProxyProtocol: "Direct",
						},
					},
				},
			},
			expectedError: nil,
		},
		{
			name:       "v1beta1 using deprecated 'master' type",
			createFile: true,
			contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "cluster"
  connection:
    proxyProtocol: "HTTPConnect"
    transport:
      tcp:
        url: "https://127.0.0.1:8131"
        tlsConfig:
          caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt"
          clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key"
          clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt"
- name: "master"
  connection:
    proxyProtocol: "HTTPConnect"
    transport:
      tcp:
        url: "https://127.0.0.1:8132"
        tlsConfig:
          caBundle: "/etc/srv/kubernetes/pki/konnectivity-server-master/ca.crt"
          clientKey: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.key"
          clientCert: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.crt"
- name: "etcd"
  connection:
    proxyProtocol: "Direct"
`,
			expectedResult: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: "HTTPConnect",
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://127.0.0.1:8131",

									TLSConfig: &apiserver.TLSConfig{
										CABundle:   "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt",
										ClientKey:  "/etc/srv/kubernetes/pki/konnectivity-server/client.key",
										ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt",
									},
								},
							},
						},
					},
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: "HTTPConnect",
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://127.0.0.1:8132",
									TLSConfig: &apiserver.TLSConfig{
										CABundle:   "/etc/srv/kubernetes/pki/konnectivity-server-master/ca.crt",
										ClientKey:  "/etc/srv/kubernetes/pki/konnectivity-server-master/client.key",
										ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server-master/client.crt",
									},
								},
							},
						},
					},
					{
						Name: "etcd",
						Connection: apiserver.Connection{
							ProxyProtocol: "Direct",
						},
					},
				},
			},
			expectedError: nil,
		},
		{
			name:       "wrong_type",
			createFile: true,
			contents: `
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    k8s-app: konnectivity-agent
  namespace: kube-system
  name: proxy-agent
spec:
  selector:
    matchLabels:
      k8s-app: konnectivity-agent
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        k8s-app: proxy-agent
    spec:
      priorityClassName: system-cluster-critical
      # Necessary to reboot node
      hostPID: true
      volumes:
        - name: pki
          hostPath:
            path: /etc/srv/kubernetes/pki/konnectivity-agent
      containers:
        - image: registry.k8s.io/proxy-agent:v0.0.3
          name: proxy-agent
          command: ["/proxy-agent"]
          args: ["--caCert=/etc/srv/kubernetes/pki/proxy-agent/ca.crt", "--agentCert=/etc/srv/kubernetes/pki/proxy-agent/client.crt", "--agentKey=/etc/srv/kubernetes/pki/proxy-agent/client.key", "--proxyServerHost=127.0.0.1", "--proxyServerPort=8132"]
          securityContext:
            capabilities:
              add: ["SYS_BOOT"]
          env:
            - name: wrong-type
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: kube-system
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            limits:
              cpu: 50m
              memory: 30Mi
          volumeMounts:
            - name: pki
              mountPath: /etc/srv/kubernetes/pki/konnectivity-agent
`,
			expectedResult: nil,
			expectedError:  ptr.To("invalid service configuration object \"DaemonSet\""),
		},
		{
			name:       "v1beta1 with tlsServerName",
			createFile: true,
			contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "cluster"
  connection:
    proxyProtocol: "HTTPConnect"
    transport:
      tcp:
        url: "https://proxy.kube-system.svc.cluster.local:8443"
        tlsConfig:
          caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt"
          clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key"
          clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt"
          tlsServerName: "konnectivity-server.example.com"
`,
			expectedResult: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: "HTTPConnect",
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://proxy.kube-system.svc.cluster.local:8443",
									TLSConfig: &apiserver.TLSConfig{
										CABundle:      "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt",
										ClientKey:     "/etc/srv/kubernetes/pki/konnectivity-server/client.key",
										ClientCert:    "/etc/srv/kubernetes/pki/konnectivity-server/client.crt",
										TLSServerName: "konnectivity-server.example.com",
									},
								},
							},
						},
					},
				},
			},
			expectedError: nil,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			proxyConfig := fmt.Sprintf("test-egress-selector-config-%s", tc.name)
			if tc.createFile {
				f, err := os.CreateTemp("", proxyConfig)
				if err != nil {
					t.Fatal(err)
				}
				defer utiltesting.CloseAndRemove(t, f)
				if err := os.WriteFile(f.Name(), []byte(tc.contents), os.FileMode(0755)); err != nil {
					t.Fatal(err)
				}
				proxyConfig = f.Name()
			}
			config, err := ReadEgressSelectorConfiguration(proxyConfig)
			if err == nil && tc.expectedError != nil {
				t.Errorf("calling ReadEgressSelectorConfiguration expected error: %s, did not get it", *tc.expectedError)
			}
			if err != nil && tc.expectedError == nil {
				t.Errorf("unexpected error calling ReadEgressSelectorConfiguration got: %#v", err)
			}
			if err != nil && tc.expectedError != nil && strings.Contains(err.Error(), *tc.expectedError) {
				t.Errorf("calling ReadEgressSelectorConfiguration expected error: %s, got %#v", *tc.expectedError, err)
			}
			if !reflect.DeepEqual(config, tc.expectedResult) {
				t.Errorf("problem with configuration returned from ReadEgressSelectorConfiguration expected: %#v, got: %#v", tc.expectedResult, config)
			}
		})
	}
}

func TestValidateEgressSelectorConfiguration(t *testing.T) {
	testcases := []struct {
		name        string
		expectError bool
		contents    *apiserver.EgressSelectorConfiguration
	}{
		{
			name:        "direct-valid",
			expectError: false,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolDirect,
						},
					},
				},
			},
		},
		{
			name:        "direct-invalid-transport",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolDirect,
							Transport:     &apiserver.Transport{},
						},
					},
				},
			},
		},
		{
			name:        "httpconnect-no-https",
			expectError: false,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolHTTPConnect,
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "http://127.0.0.1:8131",
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "httpconnect-https-no-cert-error",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolHTTPConnect,
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "https://127.0.0.1:8131",
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "httpconnect-tcp-uds-both-set",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolHTTPConnect,
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "http://127.0.0.1:8131",
								},
								UDS: &apiserver.UDSTransport{
									UDSName: "/etc/srv/kubernetes/konnectivity/konnectivity-server.socket",
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "httpconnect-uds",
			expectError: false,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolHTTPConnect,
							Transport: &apiserver.Transport{
								UDS: &apiserver.UDSTransport{
									UDSName: "/etc/srv/kubernetes/konnectivity/konnectivity-server.socket",
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "grpc-https-invalid",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolGRPC,
							Transport: &apiserver.Transport{
								TCP: &apiserver.TCPTransport{
									URL: "http://127.0.0.1:8131",
									TLSConfig: &apiserver.TLSConfig{
										CABundle:   "",
										ClientKey:  "",
										ClientCert: "",
									},
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "grpc-uds",
			expectError: false,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "cluster",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolGRPC,
							Transport: &apiserver.Transport{
								UDS: &apiserver.UDSTransport{
									UDSName: "/etc/srv/kubernetes/konnectivity/konnectivity-server.socket",
								},
							},
						},
					},
				},
			},
		},
		{
			name:        "invalid egress selection name",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "invalid",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolDirect,
						},
					},
				},
			},
		},
		{
			name:        "duplicate egress selections configured",
			expectError: true,
			contents: &apiserver.EgressSelectorConfiguration{
				TypeMeta: metav1.TypeMeta{
					Kind:       "",
					APIVersion: "",
				},
				EgressSelections: []apiserver.EgressSelection{
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolDirect,
						},
					},
					{
						Name: "controlplane",
						Connection: apiserver.Connection{
							ProxyProtocol: apiserver.ProtocolDirect,
						},
					},
				},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			errs := ValidateEgressSelectorConfiguration(tc.contents)
			if !tc.expectError && len(errs) != 0 {
				t.Errorf("Calling ValidateEgressSelectorConfiguration expected no error, got %v", errs)
			} else if tc.expectError && len(errs) == 0 {
				t.Errorf("Calling ValidateEgressSelectorConfiguration expected error, got no error")
			}
		})
	}
}
