vitess.io/vitess@v0.16.2/go/test/endtoend/vault/vault_test.go (about)

     1  /*
     2  Copyright 2020 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vault
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	"fmt"
    24  	"net"
    25  	"os"
    26  	"os/exec"
    27  	"path"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/stretchr/testify/require"
    33  
    34  	"vitess.io/vitess/go/mysql"
    35  	"vitess.io/vitess/go/test/endtoend/cluster"
    36  	"vitess.io/vitess/go/vt/log"
    37  )
    38  
    39  var (
    40  	createTable = `create table product (id bigint(20) primary key, name char(10), created bigint(20));`
    41  	insertTable = `insert into product (id, name, created) values(%d, '%s', unix_timestamp());`
    42  )
    43  
    44  var (
    45  	clusterInstance *cluster.LocalProcessCluster
    46  
    47  	primary *cluster.Vttablet
    48  	replica *cluster.Vttablet
    49  
    50  	cell            = "zone1"
    51  	hostname        = "localhost"
    52  	keyspaceName    = "ks"
    53  	shardName       = "0"
    54  	dbName          = "vt_ks"
    55  	mysqlUsers      = []string{"vt_dba", "vt_app", "vt_appdebug", "vt_repl", "vt_filtered"}
    56  	mysqlPassword   = "password"
    57  	vtgateUser      = "vtgate_user"
    58  	vtgatePassword  = "password123"
    59  	commonTabletArg = []string{
    60  		"--vreplication_healthcheck_topology_refresh", "1s",
    61  		"--vreplication_healthcheck_retry_delay", "1s",
    62  		"--vreplication_retry_delay", "1s",
    63  		"--degraded_threshold", "5s",
    64  		"--lock_tables_timeout", "5s",
    65  		"--watch_replication_stream",
    66  		// Frequently reload schema, generating some tablet traffic,
    67  		//   so we can speed up token refresh
    68  		"--queryserver-config-schema-reload-time", "5",
    69  		"--serving_state_grace_period", "1s"}
    70  	vaultTabletArg = []string{
    71  		"--db-credentials-server", "vault",
    72  		"--db-credentials-vault-timeout", "3s",
    73  		"--db-credentials-vault-path", "kv/prod/dbcreds",
    74  		// This is overriden by our env VAULT_ADDR
    75  		"--db-credentials-vault-addr", "https://127.0.0.1:8200",
    76  		// This is overriden by our env VAULT_CACERT
    77  		"--db-credentials-vault-tls-ca", "/path/to/ca.pem",
    78  		// This is provided by our env VAULT_ROLEID
    79  		//"--db-credentials-vault-roleid", "34644576-9ffc-8bb5-d046-4a0e41194e15",
    80  		// Contents of this file provided by our env VAULT_SECRETID
    81  		//"--db-credentials-vault-secretidfile", "/path/to/file/containing/secret_id",
    82  		// Make this small, so we can get a renewal
    83  		"--db-credentials-vault-ttl", "21s"}
    84  	vaultVTGateArg = []string{
    85  		"--mysql_auth_server_impl", "vault",
    86  		"--mysql_auth_vault_timeout", "3s",
    87  		"--mysql_auth_vault_path", "kv/prod/vtgatecreds",
    88  		// This is overriden by our env VAULT_ADDR
    89  		"--mysql_auth_vault_addr", "https://127.0.0.1:8200",
    90  		// This is overriden by our env VAULT_CACERT
    91  		"--mysql_auth_vault_tls_ca", "/path/to/ca.pem",
    92  		// This is provided by our env VAULT_ROLEID
    93  		//"--mysql_auth_vault_roleid", "34644576-9ffc-8bb5-d046-4a0e41194e15",
    94  		// Contents of this file provided by our env VAULT_SECRETID
    95  		//"--mysql_auth_vault_role_secretidfile", "/path/to/file/containing/secret_id",
    96  		// Make this small, so we can get a renewal
    97  		"--mysql_auth_vault_ttl", "21s"}
    98  	mysqlctlArg = []string{
    99  		"--db_dba_password", mysqlPassword}
   100  	vttabletLogFileName = "vttablet.INFO"
   101  	tokenRenewalString  = "Vault client status: token renewed"
   102  )
   103  
   104  func TestVaultAuth(t *testing.T) {
   105  	defer cluster.PanicHandler(nil)
   106  
   107  	// Instantiate Vitess Cluster objects and start topo
   108  	initializeClusterEarly(t)
   109  	defer clusterInstance.Teardown()
   110  
   111  	// start Vault server
   112  	vs := startVaultServer(t)
   113  	defer vs.stop()
   114  
   115  	// Wait for Vault server to come up
   116  	for i := 0; i < 60; i++ {
   117  		time.Sleep(250 * time.Millisecond)
   118  		ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", hostname, vs.port1))
   119  		if err != nil {
   120  			// Vault is now up, we can continue
   121  			break
   122  		}
   123  		ln.Close()
   124  	}
   125  
   126  	roleID, secretID := setupVaultServer(t, vs)
   127  	require.NotEmpty(t, roleID)
   128  	require.NotEmpty(t, secretID)
   129  
   130  	// Passing via environment, easier than trying to modify
   131  	// vtgate/vttablet flags within our test machinery
   132  	os.Setenv("VAULT_ROLEID", roleID)
   133  	os.Setenv("VAULT_SECRETID", secretID)
   134  
   135  	// Bring up rest of the Vitess cluster
   136  	initializeClusterLate(t)
   137  
   138  	// Create a table
   139  	_, err := primary.VttabletProcess.QueryTablet(createTable, keyspaceName, true)
   140  	require.NoError(t, err)
   141  
   142  	// This tests the vtgate Vault auth & indirectly vttablet Vault auth too
   143  	insertRow(t, 1, "prd-1")
   144  	insertRow(t, 2, "prd-2")
   145  
   146  	cluster.VerifyRowsInTabletForTable(t, replica, keyspaceName, 2, "product")
   147  
   148  	// Sleep for a while; giving enough time for a token renewal
   149  	//   and it making it into the (asynchronous) log
   150  	time.Sleep(30 * time.Second)
   151  	// Check the log for the Vault token renewal message
   152  	//   If we don't see it, that is a test failure
   153  	logContents, _ := os.ReadFile(path.Join(clusterInstance.TmpDirectory, vttabletLogFileName))
   154  	require.True(t, bytes.Contains(logContents, []byte(tokenRenewalString)))
   155  }
   156  
   157  func startVaultServer(t *testing.T) *Server {
   158  	vs := &Server{
   159  		address: hostname,
   160  		port1:   clusterInstance.GetAndReservePort(),
   161  		port2:   clusterInstance.GetAndReservePort(),
   162  	}
   163  	err := vs.start()
   164  	require.NoError(t, err)
   165  
   166  	return vs
   167  }
   168  
   169  // Setup everything we need in the Vault server
   170  func setupVaultServer(t *testing.T, vs *Server) (string, string) {
   171  	// The setup script uses these environment variables
   172  	//   We also reuse VAULT_ADDR and VAULT_CACERT later on
   173  	os.Setenv("VAULT", vs.execPath)
   174  	os.Setenv("VAULT_ADDR", fmt.Sprintf("https://%s:%d", vs.address, vs.port1))
   175  	os.Setenv("VAULT_CACERT", path.Join(os.Getenv("PWD"), vaultCAFileName))
   176  	setup := exec.Command(
   177  		"/bin/bash",
   178  		path.Join(os.Getenv("PWD"), vaultSetupScript),
   179  	)
   180  
   181  	logFilePath := path.Join(vs.logDir, "log_setup.txt")
   182  	logFile, _ := os.Create(logFilePath)
   183  	setup.Stderr = logFile
   184  	setup.Stdout = logFile
   185  
   186  	setup.Env = append(setup.Env, os.Environ()...)
   187  	log.Infof("Running Vault setup command: %v", strings.Join(setup.Args, " "))
   188  	err := setup.Start()
   189  	if err != nil {
   190  		log.Errorf("Error during Vault setup: %v", err)
   191  	}
   192  
   193  	setup.Wait()
   194  	var secretID, roleID string
   195  	file, err := os.Open(logFilePath)
   196  	if err != nil {
   197  		log.Error(err)
   198  	}
   199  	defer file.Close()
   200  
   201  	scanner := bufio.NewScanner(file)
   202  	for scanner.Scan() {
   203  		if strings.HasPrefix(scanner.Text(), "ROLE_ID=") {
   204  			roleID = strings.Split(scanner.Text(), "=")[1]
   205  		} else if strings.HasPrefix(scanner.Text(), "SECRET_ID=") {
   206  			secretID = strings.Split(scanner.Text(), "=")[1]
   207  		}
   208  	}
   209  	if err := scanner.Err(); err != nil {
   210  		log.Error(err)
   211  	}
   212  
   213  	return roleID, secretID
   214  }
   215  
   216  // Setup cluster object and start topo
   217  //
   218  //	We need this before vault, because we re-use the port reservation code
   219  func initializeClusterEarly(t *testing.T) {
   220  	clusterInstance = cluster.NewCluster(cell, hostname)
   221  
   222  	// Start topo server
   223  	err := clusterInstance.StartTopo()
   224  	require.NoError(t, err)
   225  }
   226  
   227  func initializeClusterLate(t *testing.T) {
   228  	// Start keyspace
   229  	keyspace := &cluster.Keyspace{
   230  		Name: keyspaceName,
   231  	}
   232  	clusterInstance.Keyspaces = append(clusterInstance.Keyspaces, *keyspace)
   233  	shard := &cluster.Shard{
   234  		Name: shardName,
   235  	}
   236  
   237  	primary = clusterInstance.NewVttabletInstance("replica", 0, "")
   238  	// We don't really need the replica to test this feature
   239  	//   but keeping it in to excercise the vt_repl user/password path
   240  	replica = clusterInstance.NewVttabletInstance("replica", 0, "")
   241  
   242  	shard.Vttablets = []*cluster.Vttablet{primary, replica}
   243  
   244  	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, commonTabletArg...)
   245  	clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, vaultTabletArg...)
   246  	clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, vaultVTGateArg...)
   247  
   248  	err := clusterInstance.SetupCluster(keyspace, []cluster.Shard{*shard})
   249  	require.NoError(t, err)
   250  	vtctldClientProcess := cluster.VtctldClientProcessInstance("localhost", clusterInstance.VtctldProcess.GrpcPort, clusterInstance.TmpDirectory)
   251  	out, err := vtctldClientProcess.ExecuteCommandWithOutput("SetKeyspaceDurabilityPolicy", keyspaceName, "--durability-policy=semi_sync")
   252  	require.NoError(t, err, out)
   253  
   254  	// Start MySQL
   255  	var mysqlCtlProcessList []*exec.Cmd
   256  	for _, shard := range clusterInstance.Keyspaces[0].Shards {
   257  		for _, tablet := range shard.Vttablets {
   258  			proc, err := tablet.MysqlctlProcess.StartProcess()
   259  			require.NoError(t, err)
   260  			mysqlCtlProcessList = append(mysqlCtlProcessList, proc)
   261  		}
   262  	}
   263  
   264  	// Wait for MySQL startup
   265  	for _, proc := range mysqlCtlProcessList {
   266  		err = proc.Wait()
   267  		require.NoError(t, err)
   268  	}
   269  
   270  	for _, tablet := range []*cluster.Vttablet{primary, replica} {
   271  		for _, user := range mysqlUsers {
   272  			query := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s';", user, hostname, mysqlPassword)
   273  			_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
   274  			// Reset after the first ALTER, or we lock ourselves out.
   275  			tablet.VttabletProcess.DbPassword = mysqlPassword
   276  			if err != nil {
   277  				query = fmt.Sprintf("ALTER USER '%s'@'%%' IDENTIFIED BY '%s';", user, mysqlPassword)
   278  				_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
   279  				require.NoError(t, err)
   280  			}
   281  		}
   282  		query := fmt.Sprintf("create database %s;", dbName)
   283  		_, err = tablet.VttabletProcess.QueryTablet(query, keyspace.Name, false)
   284  		require.NoError(t, err)
   285  
   286  		err = tablet.VttabletProcess.Setup()
   287  		require.NoError(t, err)
   288  
   289  		// Modify mysqlctl password too, or teardown will be locked out
   290  		tablet.MysqlctlProcess.ExtraArgs = append(tablet.MysqlctlProcess.ExtraArgs, mysqlctlArg...)
   291  	}
   292  
   293  	err = clusterInstance.VtctlclientProcess.InitShardPrimary(keyspaceName, shard.Name, cell, primary.TabletUID)
   294  	require.NoError(t, err)
   295  
   296  	err = clusterInstance.StartVTOrc(keyspaceName)
   297  	require.NoError(t, err)
   298  
   299  	// Start vtgate
   300  	err = clusterInstance.StartVtgate()
   301  	require.NoError(t, err)
   302  }
   303  
   304  func insertRow(t *testing.T, id int, productName string) {
   305  	ctx := context.Background()
   306  	vtParams := mysql.ConnParams{
   307  		Host:  clusterInstance.Hostname,
   308  		Port:  clusterInstance.VtgateMySQLPort,
   309  		Uname: vtgateUser,
   310  		Pass:  vtgatePassword,
   311  	}
   312  	conn, err := mysql.Connect(ctx, &vtParams)
   313  	require.NoError(t, err)
   314  	defer conn.Close()
   315  
   316  	insertSmt := fmt.Sprintf(insertTable, id, productName)
   317  	_, err = conn.ExecuteFetch(insertSmt, 1000, true)
   318  	require.NoError(t, err)
   319  }