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 }