package envoy

import (
	"encoding/json"
	"flag"
	"github.com/stretchr/testify/assert"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/hashicorp/consul/agent"
	"github.com/hashicorp/consul/agent/xds"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/mitchellh/cli"
	"github.com/stretchr/testify/require"
)

var update = flag.Bool("update", false, "update golden files")

func TestEnvoyCommand_noTabs(t *testing.T) {
	t.Parallel()
	if strings.ContainsRune(New(nil).Help(), '\t') {
		t.Fatal("help has tabs")
	}
}

func TestEnvoyGateway_Validation(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name   string
		args   []string
		output string
	}{
		{
			"-register for non-gateway",
			[]string{"-register", "-proxy-id", "not-a-gateway"},
			"Auto-Registration can only be used for gateways",
		},
		{
			"-mesh-gateway and -gateway cannot be combined",
			[]string{"-register", "-mesh-gateway", "-gateway", "mesh"},
			"The mesh-gateway flag is deprecated and cannot be used alongside the gateway flag",
		},
		{
			"no proxy registration specified nor discovered",
			[]string{""},
			"No proxy ID specified",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			c := New(ui)
			c.init()

			code := c.Run(tc.args)
			if code == 0 {
				t.Errorf("%s: expected non-zero exit", tc.name)
			}

			output := ui.ErrorWriter.String()
			if !strings.Contains(output, tc.output) {
				t.Errorf("expected %q to contain %q", output, tc.output)
			}
		})
	}
}

// testSetAndResetEnv sets the env vars passed as KEY=value strings in the
// current ENV and returns a func() that will undo it's work at the end of the
// test for use with defer.
func testSetAndResetEnv(t *testing.T, env []string) func() {
	old := make(map[string]*string)
	for _, e := range env {
		pair := strings.SplitN(e, "=", 2)
		current := os.Getenv(pair[0])
		if current != "" {
			old[pair[0]] = &current
		} else {
			// save it as a nil so we know to remove again
			old[pair[0]] = nil
		}
		os.Setenv(pair[0], pair[1])
	}
	// Return a func that will reset to old values
	return func() {
		for k, v := range old {
			if v == nil {
				os.Unsetenv(k)
			} else {
				os.Setenv(k, *v)
			}
		}
	}
}

// This tests the args we use to generate the template directly because they
// encapsulate all the argument and default handling code which is where most of
// the logic is. We also allow generating golden files but only for cases that
// pass the test of having their template args generated as expected.
func TestGenerateConfig(t *testing.T) {
	cases := []struct {
		Name        string
		Flags       []string
		Env         []string
		Files       map[string]string
		ProxyConfig map[string]interface{}
		GRPCPort    int // only used for testing custom-configured grpc port
		WantArgs    BootstrapTplArgs
		WantErr     string
	}{
		{
			Name:    "no-args",
			Flags:   []string{},
			Env:     []string{},
			WantErr: "No proxy ID specified",
		},
		{
			Name:  "defaults",
			Flags: []string{"-proxy-id", "test-proxy"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502", // Note this is the gRPC port
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name: "token-arg",
			Flags: []string{"-proxy-id", "test-proxy",
				"-token", "c9a52720-bf6c-4aa6-b8bc-66881a5ade95"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502", // Note this is the gRPC port
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
				Token:                 "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
		},
		{
			Name:  "token-env",
			Flags: []string{"-proxy-id", "test-proxy"},
			Env: []string{
				"CONSUL_HTTP_TOKEN=c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502", // Note this is the gRPC port
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
				Token:                 "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
		},
		{
			Name: "token-file-arg",
			Flags: []string{"-proxy-id", "test-proxy",
				"-token-file", "@@TEMPDIR@@token.txt",
			},
			Files: map[string]string{
				"token.txt": "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502", // Note this is the gRPC port
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
				Token:                 "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
		},
		{
			Name:  "token-file-env",
			Flags: []string{"-proxy-id", "test-proxy"},
			Env: []string{
				"CONSUL_HTTP_TOKEN_FILE=@@TEMPDIR@@token.txt",
			},
			Files: map[string]string{
				"token.txt": "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502", // Note this is the gRPC port
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
				Token:                 "c9a52720-bf6c-4aa6-b8bc-66881a5ade95",
			},
		},
		{
			Name: "grpc-addr-flag",
			Flags: []string{"-proxy-id", "test-proxy",
				"-grpc-addr", "localhost:9999"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "9999",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "grpc-addr-env",
			Flags: []string{"-proxy-id", "test-proxy"},
			Env: []string{
				"CONSUL_GRPC_ADDR=localhost:9999",
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "9999",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name: "grpc-addr-unix",
			Flags: []string{"-proxy-id", "test-proxy",
				"-grpc-addr", "unix:///var/run/consul.sock"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentSocket: "/var/run/consul.sock",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:     "grpc-addr-config",
			Flags:    []string{"-proxy-id", "test-proxy"},
			GRPCPort: 9999,
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "9999",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "access-log-path",
			Flags: []string{"-proxy-id", "test-proxy", "-admin-access-log-path", "/some/path/access.log"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/some/path/access.log",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "missing-ca-file",
			Flags: []string{"-proxy-id", "test-proxy", "-ca-file", "some/path"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
			},
			WantErr: "Error loading CA File: open some/path: no such file or directory",
		},
		{
			Name:  "existing-ca-file",
			Flags: []string{"-proxy-id", "test-proxy", "-ca-file", "../../../test/ca/root.cer"},
			Env:   []string{"CONSUL_HTTP_SSL=1"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
					AgentTLS:     true,
				},
				AgentCAPEM:            `-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+gAwIBAgIJAIewRMI8OnvTMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHDAa\nBgNVBAoTE0hhc2hpQ29ycCBUZXN0IENlcnQxDDAKBgNVBAsTA0RldjEWMBQGA1UE\nAxMNdGVzdC5pbnRlcm5hbDEgMB4GCSqGSIb3DQEJARYRdGVzdEBpbnRlcm5hbC5j\nb20wHhcNMTQwNDA3MTkwMTA4WhcNMjQwNDA0MTkwMTA4WjCBmDELMAkGA1UEBhMC\nVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQK\nExNIYXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRl\nc3QuaW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrs6JK4NpiOItxrpNR/1ppUU\nmH7p2BgLCBZ6eHdclle9J56i68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsU\nd0HejRqfHRMtg2O+UQkv4Z66+Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcG\nxcFZzTRFFZt2vPnyHj8cHcaFo/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvg\nU2jQuC7Fhnl33Zd3B6d3mQH6O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/f\npFIIcHSiHRfoqHLGsGg+3swId/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQID\nAQABo4IBADCB/TAdBgNVHQ4EFgQUo/nrOfqvbee2VklVKIFlyQEbuJUwgc0GA1Ud\nIwSBxTCBwoAUo/nrOfqvbee2VklVKIFlyQEbuJWhgZ6kgZswgZgxCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UE\nChMTSGFzaGlDb3JwIFRlc3QgQ2VydDEMMAoGA1UECxMDRGV2MRYwFAYDVQQDEw10\nZXN0LmludGVybmFsMSAwHgYJKoZIhvcNAQkBFhF0ZXN0QGludGVybmFsLmNvbYIJ\nAIewRMI8OnvTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADa9fV9h\ngjapBlkNmu64WX0Ufub5dsJrdHS8672P30S7ILB7Mk0W8sL65IezRsZnG898yHf9\n2uzmz5OvNTM9K380g7xFlyobSVq+6yqmmSAlA/ptAcIIZT727P5jig/DB7fzJM3g\njctDlEGOmEe50GQXc25VKpcpjAsNQi5ER5gowQ0v3IXNZs+yU+LvxLHc0rUJ/XSp\nlFCAMOqd5uRoMOejnT51G6krvLNzPaQ3N9jQfNVY4Q0zfs0M+6dRWvqfqB9Vyq8/\nPOLMld+HyAZEBk9zK3ZVIXx6XS4dkDnSNR91njLq7eouf6M7+7s/oMQZZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----\n`,
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "missing-ca-path",
			Flags: []string{"-proxy-id", "test-proxy", "-ca-path", "some/path"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
			},
			WantErr: "lstat some/path: no such file or directory",
		},
		{
			Name:  "existing-ca-path",
			Flags: []string{"-proxy-id", "test-proxy", "-ca-path", "../../../test/ca_path/"},
			Env:   []string{"CONSUL_HTTP_SSL=1"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
					AgentTLS:     true,
				},
				AgentCAPEM:            `-----BEGIN CERTIFICATE-----\nMIIFADCCAuqgAwIBAgIBATALBgkqhkiG9w0BAQswEzERMA8GA1UEAxMIQ2VydEF1\ndGgwHhcNMTUwNTExMjI0NjQzWhcNMjUwNTExMjI0NjU0WjATMREwDwYDVQQDEwhD\nZXJ0QXV0aDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALcMByyynHsA\n+K4PJwo5+XHygaEZAhPGvHiKQK2Cbc9NDm0ZTzx0rA/dRTZlvouhDyzcJHm+6R1F\nj6zQv7iaSC3qQtJiPnPsfZ+/0XhFZ3fQWMnfDiGbZpF1kJF01ofB6vnsuocFC0zG\naGC+SZiLAzs+QMP3Bebw1elCBIeoN+8NWnRYmLsYIaYGJGBSbNo/lCpLTuinofUn\nL3ehWEGv1INwpHnSVeN0Ml2GFe23d7PUlj/wNIHgUdpUR+KEJxIP3klwtsI3QpSH\nc4VjWdf4aIcka6K3IFuw+K0PUh3xAAPnMpAQOtCZk0AhF5rlvUbevC6jADxpKxLp\nOONmvCTer4LtyNURAoBH52vbK0r/DNcTpPEFV0IP66nXUFgkk0mRKsu8HTb4IOkC\nX3K4mp18EiWUUtrHZAnNct0iIniDBqKK0yhSNhztG6VakVt/1WdQY9Ey3mNtxN1O\nthqWFKdpKUzPKYC3P6PfVpiE7+VbWTLLXba+8BPe8BxWPsVkjJqGSGnCte4COusz\nM8/7bbTgifwJfsepwFtZG53tvwjWlO46Exl30VoDNTaIGvs1fO0GqJlh2A7FN5F2\nS1rS5VYHtPK8QdmUSvyq+7JDBc1HNT5I2zsIQbNcLwDTZ5EsbU6QR7NHDJKxjv/w\nbs3eTXJSSNcFD74wRU10pXjgE5wOFu9TAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIA\nBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQHazgZ3Puiuc6K2LzgcX5b6fAC\nPzAfBgNVHSMEGDAWgBQHazgZ3Puiuc6K2LzgcX5b6fACPzALBgkqhkiG9w0BAQsD\nggIBAEmeNrSUhpHg1I8dtfqu9hCU/6IZThjtcFA+QcPkkMa+Z1k0SOtsgW8MdlcA\ngCf5g5yQZ0DdpWM9nDB6xDIhQdccm91idHgf8wmpEHUj0an4uyn2ESCt8eqrAWf7\nAClYORCASTYfguJCxcfvwtI1uqaOeCxSOdmFay79UVitVsWeonbCRGsVgBDifJxw\nG2oCQqoYAmXPM4J6syk5GHhB1O9MMq+g1+hOx9s+XHyTui9FL4V+IUO1ygVqEQB5\nPSiRBvcIsajSGVao+vK0gf2XfcXzqr3y3NhBky9rFMp1g+ykb2yWekV4WiROJlCj\nTsWwWZDRyjiGahDbho/XW8JciouHZhJdjhmO31rqW3HdFviCTdXMiGk3GQIzz/Jg\nP+enOaHXoY9lcxzDvY9z1BysWBgNvNrMnVge/fLP9o+a0a0PRIIVl8T0Ef3zeg1O\nCLCSy/1Vae5Tx63ZTFvGFdOSusYkG9rlAUHXZE364JRCKzM9Bz0bM+t+LaO0MaEb\nYoxcXEPU+gB2IvmARpInN3oHexR6ekuYHVTRGdWrdmuHFzc7eFwygRqTFdoCCU+G\nQZEkd+lOEyv0zvQqYg+Jp0AEGz2B2zB53uBVECtn0EqrSdPtRzUBSByXVs6QhSXn\neVmy+z3U3MecP63X6oSPXekqSyZFuegXpNNuHkjNoL4ep2ix\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+gAwIBAgIJAIewRMI8OnvTMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHDAa\nBgNVBAoTE0hhc2hpQ29ycCBUZXN0IENlcnQxDDAKBgNVBAsTA0RldjEWMBQGA1UE\nAxMNdGVzdC5pbnRlcm5hbDEgMB4GCSqGSIb3DQEJARYRdGVzdEBpbnRlcm5hbC5j\nb20wHhcNMTQwNDA3MTkwMTA4WhcNMjQwNDA0MTkwMTA4WjCBmDELMAkGA1UEBhMC\nVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQK\nExNIYXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRl\nc3QuaW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrs6JK4NpiOItxrpNR/1ppUU\nmH7p2BgLCBZ6eHdclle9J56i68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsU\nd0HejRqfHRMtg2O+UQkv4Z66+Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcG\nxcFZzTRFFZt2vPnyHj8cHcaFo/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvg\nU2jQuC7Fhnl33Zd3B6d3mQH6O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/f\npFIIcHSiHRfoqHLGsGg+3swId/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQID\nAQABo4IBADCB/TAdBgNVHQ4EFgQUo/nrOfqvbee2VklVKIFlyQEbuJUwgc0GA1Ud\nIwSBxTCBwoAUo/nrOfqvbee2VklVKIFlyQEbuJWhgZ6kgZswgZgxCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UE\nChMTSGFzaGlDb3JwIFRlc3QgQ2VydDEMMAoGA1UECxMDRGV2MRYwFAYDVQQDEw10\nZXN0LmludGVybmFsMSAwHgYJKoZIhvcNAQkBFhF0ZXN0QGludGVybmFsLmNvbYIJ\nAIewRMI8OnvTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADa9fV9h\ngjapBlkNmu64WX0Ufub5dsJrdHS8672P30S7ILB7Mk0W8sL65IezRsZnG898yHf9\n2uzmz5OvNTM9K380g7xFlyobSVq+6yqmmSAlA/ptAcIIZT727P5jig/DB7fzJM3g\njctDlEGOmEe50GQXc25VKpcpjAsNQi5ER5gowQ0v3IXNZs+yU+LvxLHc0rUJ/XSp\nlFCAMOqd5uRoMOejnT51G6krvLNzPaQ3N9jQfNVY4Q0zfs0M+6dRWvqfqB9Vyq8/\nPOLMld+HyAZEBk9zK3ZVIXx6XS4dkDnSNR91njLq7eouf6M7+7s/oMQZZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----\n`,
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "custom-bootstrap",
			Flags: []string{"-proxy-id", "test-proxy"},
			ProxyConfig: map[string]interface{}{
				// Add a completely custom bootstrap template. Never mind if this is
				// invalid envoy config just as long as it works and gets the variables
				// interplated.
				"envoy_bootstrap_json_tpl": `
				{
					"admin": {
						"access_log_path": "/dev/null",
						"address": {
							"socket_address": {
								"address": "{{ .AdminBindAddress }}",
								"port_value": {{ .AdminBindPort }}
							}
						}
					},
					"node": {
						"cluster": "{{ .ProxyCluster }}",
						"id": "{{ .ProxyID }}"
					},
					"custom_field": "foo"
				}`,
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "extra_-single",
			Flags: []string{"-proxy-id", "test-proxy"},
			ProxyConfig: map[string]interface{}{
				// Add a custom sections with interpolated variables. These are all
				// invalid config syntax too but we are just testing they have the right
				// effect.
				"envoy_extra_static_clusters_json": `
				{
					"name": "fake_cluster_1"
				}`,
				"envoy_extra_static_listeners_json": `
				{
					"name": "fake_listener_1"
				}`,
				"envoy_extra_stats_sinks_json": `
				{
					"name": "fake_sink_1"
				}`,
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "extra_-multiple",
			Flags: []string{"-proxy-id", "test-proxy"},
			ProxyConfig: map[string]interface{}{
				// Add a custom sections with interpolated variables. These are all
				// invalid config syntax too but we are just testing they have the right
				// effect.
				"envoy_extra_static_clusters_json": `
				{
					"name": "fake_cluster_1"
				},
				{
					"name": "fake_cluster_2"
				}`,
				"envoy_extra_static_listeners_json": `
				{
					"name": "fake_listener_1"
				},{
					"name": "fake_listener_2"
				}`,
				"envoy_extra_stats_sinks_json": `
				{
					"name": "fake_sink_1"
				} , { "name": "fake_sink_2" }`,
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "stats-config-override",
			Flags: []string{"-proxy-id", "test-proxy"},
			ProxyConfig: map[string]interface{}{
				// Add a custom sections with interpolated variables. These are all
				// invalid config syntax too but we are just testing they have the right
				// effect.
				"envoy_stats_config_json": `
				{
					"name": "fake_config"
				}`,
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "zipkin-tracing-config",
			Flags: []string{"-proxy-id", "test-proxy"},
			ProxyConfig: map[string]interface{}{
				// Add a custom sections with interpolated variables. These are all
				// invalid config syntax too but we are just testing they have the right
				// effect.
				"envoy_tracing_json": `{
					"http": {
						"name": "envoy.zipkin",
						"config": {
							"collector_cluster": "zipkin",
							"collector_endpoint": "/api/v1/spans"
						}
					}
				}`,
				// Need to setup the cluster to send that too as well
				"envoy_extra_static_clusters_json": `{
					"name": "zipkin",
					"type": "STRICT_DNS",
					"connect_timeout": "5s",
					"load_assignment": {
						"cluster_name": "zipkin",
						"endpoints": [
							{
								"lb_endpoints": [
									{
										"endpoint": {
											"address": {
												"socket_address": {
													"address": "zipkin.service.consul",
													"port_value": 9411
												}
											}
										}
									}
								]
							}
						]
					}
				}`,
			},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "CONSUL_HTTP_ADDR-with-https-scheme-enables-tls",
			Flags: []string{"-proxy-id", "test-proxy"},
			Env:   []string{"CONSUL_HTTP_ADDR=https://127.0.0.1:8888"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "test-proxy",
				ProxyID:      "test-proxy",
				// Should resolve IP, note this might not resolve the same way
				// everywhere which might make this test brittle but not sure what else
				// to do.
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
					AgentTLS:     true,
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "ingress-gateway",
			Flags: []string{"-proxy-id", "ingress-gateway-1", "-gateway", "ingress"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "ingress-gateway",
				ProxyID:      "ingress-gateway-1",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "ingress-gateway-address-specified",
			Flags: []string{"-proxy-id", "ingress-gateway", "-gateway", "ingress", "-address", "1.2.3.4:7777"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "ingress-gateway",
				ProxyID:      "ingress-gateway",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "ingress-gateway-register-with-service-without-proxy-id",
			Flags: []string{"-gateway", "ingress", "-register", "-service", "my-gateway", "-address", "127.0.0.1:7777"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "my-gateway",
				ProxyID:      "my-gateway",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "ingress-gateway-register-with-service-and-proxy-id",
			Flags: []string{"-gateway", "ingress", "-register", "-service", "my-gateway", "-proxy-id", "my-gateway-123", "-address", "127.0.0.1:7777"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "my-gateway",
				ProxyID:      "my-gateway-123",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
		{
			Name:  "ingress-gateway-no-auto-register",
			Flags: []string{"-gateway", "ingress", "-address", "127.0.0.1:7777"},
			WantArgs: BootstrapTplArgs{
				EnvoyVersion: defaultEnvoyVersion,
				ProxyCluster: "ingress-gateway",
				ProxyID:      "ingress-gateway",
				GRPC: GRPC{
					AgentAddress: "127.0.0.1",
					AgentPort:    "8502",
				},
				AdminAccessLogPath:    "/dev/null",
				AdminBindAddress:      "127.0.0.1",
				AdminBindPort:         "19000",
				LocalAgentClusterName: xds.LocalAgentClusterName,
			},
		},
	}

	copyAndReplaceAll := func(s []string, old, new string) []string {
		out := make([]string, len(s))
		for i, v := range s {
			out[i] = strings.ReplaceAll(v, old, new)
		}
		return out
	}

	for _, tc := range cases {
		t.Run(tc.Name, func(t *testing.T) {
			require := require.New(t)

			testDir := testutil.TempDir(t, "envoytest")
			defer os.RemoveAll(testDir)

			if len(tc.Files) > 0 {
				for fn, fv := range tc.Files {
					fullname := filepath.Join(testDir, fn)
					require.NoError(ioutil.WriteFile(fullname, []byte(fv), 0600))
				}
			}

			// Run a mock agent API that just always returns the proxy config in the
			// test.
			srv := httptest.NewServer(testMockAgent(tc.ProxyConfig, tc.GRPCPort))
			defer srv.Close()
			client, err := api.NewClient(&api.Config{Address: srv.URL})
			require.NoError(err)

			testDirPrefix := testDir + string(filepath.Separator)
			myEnv := copyAndReplaceAll(tc.Env, "@@TEMPDIR@@", testDirPrefix)
			defer testSetAndResetEnv(t, myEnv)()

			ui := cli.NewMockUi()
			c := New(ui)
			// explicitly set the client to one which can connect to the httptest.Server
			c.client = client

			// Run the command
			myFlags := copyAndReplaceAll(tc.Flags, "@@TEMPDIR@@", testDirPrefix)
			args := append([]string{"-bootstrap"}, myFlags...)

			require.NoError(c.flags.Parse(args))
			code := c.run(c.flags.Args())
			if tc.WantErr == "" {
				require.Equal(0, code, ui.ErrorWriter.String())
			} else {
				require.Equal(1, code, ui.ErrorWriter.String())
				require.Contains(ui.ErrorWriter.String(), tc.WantErr)
				return
			}

			// Verify we handled the env and flags right first to get correct template
			// args.
			got, err := c.templateArgs()
			require.NoError(err) // Error cases should have returned above
			require.Equal(&tc.WantArgs, got)

			// Actual template output goes to stdout direct to avoid prefix in UI, so
			// generate it again here to assert on.
			actual, err := c.generateConfig()
			require.NoError(err)

			// If we got the arg handling write, verify output
			golden := filepath.Join("testdata", tc.Name+".golden")
			if *update {
				ioutil.WriteFile(golden, actual, 0644)
			}

			expected, err := ioutil.ReadFile(golden)
			require.NoError(err)
			require.Equal(string(expected), string(actual))
		})
	}
}

func TestEnvoy_GatewayRegistration(t *testing.T) {
	t.Parallel()
	a := agent.NewTestAgent(t, ``)
	defer a.Shutdown()
	client := a.Client()

	tt := []struct {
		name    string
		args    []string
		kind    api.ServiceKind
		id      string
		service string
	}{
		{
			name: "register gateway with proxy-id and name",
			args: []string{
				"-http-addr=" + a.HTTPAddr(),
				"-register",
				"-bootstrap",
				"-gateway", "ingress",
				"-service", "us-ingress",
				"-proxy-id", "us-ingress-1",
			},
			kind:    api.ServiceKindIngressGateway,
			id:      "us-ingress-1",
			service: "us-ingress",
		},
		{
			name: "register gateway without proxy-id with name",
			args: []string{
				"-http-addr=" + a.HTTPAddr(),
				"-register",
				"-bootstrap",
				"-gateway", "ingress",
				"-service", "us-ingress",
			},
			kind:    api.ServiceKindIngressGateway,
			id:      "us-ingress",
			service: "us-ingress",
		},
		{
			name: "register gateway without proxy-id and without name",
			args: []string{
				"-http-addr=" + a.HTTPAddr(),
				"-register",
				"-bootstrap",
				"-gateway", "ingress",
			},
			kind:    api.ServiceKindIngressGateway,
			id:      "ingress-gateway",
			service: "ingress-gateway",
		},
		{
			name: "register gateway with proxy-id without name",
			args: []string{
				"-http-addr=" + a.HTTPAddr(),
				"-register",
				"-bootstrap",
				"-gateway", "ingress",
				"-proxy-id", "us-ingress-1",
			},
			kind:    api.ServiceKindIngressGateway,
			id:      "us-ingress-1",
			service: "ingress-gateway",
		},
	}

	for _, tc := range tt {
		t.Run(tc.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			c := New(ui)

			code := c.Run(tc.args)
			if code != 0 {
				t.Fatalf("bad exit code: %d. %#v", code, ui.ErrorWriter.String())
			}

			data, _, err := client.Agent().Service(tc.id, nil)
			assert.NoError(t, err)

			assert.NotNil(t, data)
			assert.Equal(t, tc.kind, data.Kind)
			assert.Equal(t, tc.id, data.ID)
			assert.Equal(t, tc.service, data.Service)
			assert.Equal(t, defaultGatewayPort, data.Port)
		})
	}
}

// testMockAgent combines testMockAgentProxyConfig and testMockAgentSelf,
// routing /agent/service/... requests to testMockAgentProxyConfig and
// routing /agent/self requests to testMockAgentSelf.
func testMockAgent(agentCfg map[string]interface{}, grpcPort int) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/agent/services") {
			testMockAgentGatewayConfig()(w, r)
			return
		}

		if strings.Contains(r.URL.Path, "/agent/service") {
			testMockAgentProxyConfig(agentCfg)(w, r)
			return
		}

		if strings.Contains(r.URL.Path, "/agent/self") {
			testMockAgentSelf(grpcPort)(w, r)
			return
		}

		http.NotFound(w, r)
	})
}

func testMockAgentGatewayConfig() http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Parse the proxy-id from the end of the URL (blindly assuming it's correct
		// format)
		params := r.URL.Query()
		filter := params["filter"][0]

		var kind api.ServiceKind
		switch {
		case strings.Contains(filter, string(api.ServiceKindTerminatingGateway)):
			kind = api.ServiceKindTerminatingGateway
		case strings.Contains(filter, string(api.ServiceKindIngressGateway)):
			kind = api.ServiceKindIngressGateway
		}

		svc := map[string]*api.AgentService{
			string(kind): {
				Kind:    kind,
				ID:      string(kind),
				Service: string(kind),
			},
		}

		cfgJSON, err := json.Marshal(svc)
		if err != nil {
			w.WriteHeader(500)
			w.Write([]byte(err.Error()))
			return
		}
		w.Write(cfgJSON)
	})
}

func testMockAgentProxyConfig(cfg map[string]interface{}) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Parse the proxy-id from the end of the URL (blindly assuming it's correct
		// format)
		proxyID := strings.TrimPrefix(r.URL.Path, "/v1/agent/service/")
		serviceID := strings.TrimSuffix(proxyID, "-sidecar-proxy")

		svc := api.AgentService{
			Kind:    api.ServiceKindConnectProxy,
			ID:      proxyID,
			Service: proxyID,
			Proxy: &api.AgentServiceConnectProxyConfig{
				DestinationServiceName: serviceID,
				DestinationServiceID:   serviceID,
				Config:                 cfg,
			},
		}

		cfgJSON, err := json.Marshal(svc)
		if err != nil {
			w.WriteHeader(500)
			w.Write([]byte(err.Error()))
			return
		}
		w.Write(cfgJSON)
	})
}

func TestEnvoyCommand_canBindInternal(t *testing.T) {
	t.Parallel()
	type testCheck struct {
		expected bool
		addr     string
	}

	type testCase struct {
		ifAddrs []net.Addr
		checks  map[string]testCheck
	}

	parseIPNets := func(t *testing.T, in ...string) []net.Addr {
		var out []net.Addr
		for _, addr := range in {
			ip := net.ParseIP(addr)
			require.NotNil(t, ip)
			out = append(out, &net.IPNet{IP: ip})
		}
		return out
	}

	parseIPs := func(t *testing.T, in ...string) []net.Addr {
		var out []net.Addr
		for _, addr := range in {
			ip := net.ParseIP(addr)
			require.NotNil(t, ip)
			out = append(out, &net.IPAddr{IP: ip})
		}
		return out
	}

	cases := map[string]testCase{
		"IPNet": {
			parseIPNets(t, "10.3.0.2", "198.18.0.1", "2001:db8:a0b:12f0::1"),
			map[string]testCheck{
				"ipv4": {
					true,
					"10.3.0.2",
				},
				"secondary ipv4": {
					true,
					"198.18.0.1",
				},
				"ipv6": {
					true,
					"2001:db8:a0b:12f0::1",
				},
				"ipv4 not found": {
					false,
					"1.2.3.4",
				},
				"ipv6 not found": {
					false,
					"::ffff:192.168.0.1",
				},
			},
		},
		"IPAddr": {
			parseIPs(t, "10.3.0.2", "198.18.0.1", "2001:db8:a0b:12f0::1"),
			map[string]testCheck{
				"ipv4": {
					true,
					"10.3.0.2",
				},
				"secondary ipv4": {
					true,
					"198.18.0.1",
				},
				"ipv6": {
					true,
					"2001:db8:a0b:12f0::1",
				},
				"ipv4 not found": {
					false,
					"1.2.3.4",
				},
				"ipv6 not found": {
					false,
					"::ffff:192.168.0.1",
				},
			},
		},
	}

	for name, tcase := range cases {
		t.Run(name, func(t *testing.T) {
			for checkName, check := range tcase.checks {
				t.Run(checkName, func(t *testing.T) {
					require.Equal(t, check.expected, canBindInternal(check.addr, tcase.ifAddrs))
				})
			}
		})
	}
}

// testMockAgentSelf returns an empty /v1/agent/self response except GRPC
// port is filled in to match the given wantGRPCPort argument.
func testMockAgentSelf(wantGRPCPort int) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := agent.Self{
			DebugConfig: map[string]interface{}{
				"GRPCPort": wantGRPCPort,
			},
		}

		selfJSON, err := json.Marshal(resp)
		if err != nil {
			w.WriteHeader(500)
			w.Write([]byte(err.Error()))
			return
		}
		w.Write(selfJSON)
	})
}
