Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ The riak-go-client component is used under the Apache license:
@vendor/github.com/basho/riak-go-client/*
./vendor/github.com/basho/riak-go-client/LICENSE

The dburl component is used under the MIT license:
@traffic_ops/vendor/github.com/xo/dburl/*
./traffic_ops/vendor/github.com/xo/dburl/LICENSE

The envconfig component is used under the MIT license:
@traffic_ops/vendor/github.com/kelseyhightower/envconfig/*
./traffic_ops/vendor/github.com/kelseyhightower/envconfig/LICENSE
Expand Down
129 changes: 58 additions & 71 deletions traffic_ops/app/db/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package main
*/

import (
"bytes"
"database/sql"
"errors"
"flag"
"fmt"
Expand All @@ -29,6 +29,8 @@ import (
"os/exec"
"strings"

_ "github.com/lib/pq"
"github.com/xo/dburl"
"gopkg.in/yaml.v2"
)

Expand Down Expand Up @@ -171,68 +173,73 @@ func parseDBConfig() error {
return nil
}

func createDB() {
dbExistsCmd := exec.Command("psql", "-h", HostIP, "-U", DBSuperUser, "-p", HostPort, "-tAc", "SELECT 1 FROM pg_database WHERE datname='"+DBName+"'")
out, err := dbExistsCmd.Output()
if err != nil {
die("unable to check if DB already exists: " + err.Error())
}
if len(out) > 0 {
fmt.Println("Database " + DBName + " already exists")
return
}
createDBCmd := exec.Command("createdb", "-h", HostIP, "-p", HostPort, "-U", DBSuperUser, "-e", "--owner", DBUser, DBName)
out, err = createDBCmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't create db " + DBName)
func connectAndExecute(query string, queryErrPrefix string, returnRows bool) *sql.Rows {
//todo prepared statements
//todo inspect results of executing the above query for all possible cases (db exists, db dne, db inaccessible)
//todo determine test coverage of this portion
if db, err := dburl.Open(fmt.Sprintf("pg://%s:%s@%s:%s/", DBSuperUser, DBPassword, HostIP, HostPort)); err == nil {
if !returnRows {
if _, err := db.Exec(query); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So some of our files like seeds.sql contain multiple sql queries. Doesn't this only execute a single sql query?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, but in this case it's reading an entire file in for things like patch, seed, create and executing it all at once

This has come up apparently in a few discussions on lib/pq and other repositories related to golang and postgres, and one project offers a solution (https://github.com/gchaincl/dotsql) but it requires more effort

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's preferred I could parse in the DML/DDL files and break them down into individual queries

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I thought db.Exec would return after running only a single query, hence making it not work for files like seeds.sql which have multiple queries. If that works, I'm fine with that -- no need to break them down into individual queries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 99% sure this is why ciab doesn't come up in the pipeline though, so I'll break it down into individual queries or use dotsql

die(queryErrPrefix + ":" + err.Error())
}
} else {
if rows, err := db.Query(query); err != nil {
die(queryErrPrefix + ":" + err.Error())
} else {
return rows
}
}
} else {
die("couldn't connect to pg at " + HostIP + ": " + err.Error())
}
return nil
}

func createDB() {
fmt.Println("Creating database: " + DBName)
connectAndExecute(`CREATE DATABASE `+DBName+` OWNER `+DBUser, "Couldn't create database "+DBName, false)
}

func dropDB() {
fmt.Println("Dropping database: " + DBName)
cmd := exec.Command("dropdb", "-h", HostIP, "-p", HostPort, "-U", DBSuperUser, "-e", "--if-exists", DBName)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't drop db " + DBName)
}
connectAndExecute(`DROP DATABASE `+DBName, "Couldn't drop database:"+DBName, false)
}

func createUser() {
fmt.Println("Creating user: " + DBUser)
userExistsCmd := exec.Command("psql", "-h", HostIP, "-U", DBSuperUser, "-p", HostPort, "-tAc", "SELECT 1 FROM pg_roles WHERE rolname='"+DBUser+"'")
out, err := userExistsCmd.Output()
if err != nil {
die("unable to check if user already exists: " + err.Error())
}
if len(out) > 0 {
fmt.Println("User " + DBUser + " already exists")
return
}
createUserCmd := exec.Command("psql", "-h", HostIP, "-p", HostPort, "-U", DBSuperUser, "-etAc", "CREATE USER "+DBUser+" WITH LOGIN ENCRYPTED PASSWORD '"+DBPassword+"'")
out, err = createUserCmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't create user " + DBUser)

userExistsCmd := connectAndExecute(
"SELECT 1 FROM pg_roles WHERE rolname='"+DBUser+"'",
"Unable to query for user existence "+DBUser, true)
var exists int
if userExistsCmd.Scan(&exists); exists != 1 {
fmt.Println("User does not exist, creating: " + DBUser)
connectAndExecute(
"CREATE USER "+DBUser+" WITH LOGIN ENCRYPTED PASSWORD '"+DBPassword+"'",
"Unable to create user",
false)
}
}

func dropUser() {
cmd := exec.Command("dropuser", "-h", HostIP, "-p", HostPort, "-U", DBSuperUser, "-i", "-e", DBUser)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't drop user " + DBUser)
}
// TODO do i need to handle owned schema before dropping?
connectAndExecute(`DROP ROLE `+DBUser, `Couldn't drop user `+DBUser, false)
}

func showUsers() {
cmd := exec.Command("psql", "-h", HostIP, "-p", HostPort, "-U", DBSuperUser, "-ec", `\du`)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't show users")
rows := connectAndExecute(`
SELECT usename AS role_name
FROM pg_catalog.pg_user
ORDER BY role_name desc;`, `Couldn't show users`, true)
// todo cleaner output
fmt.Println("role_name | role_attributes")
var roleName, roleAttributes string
for rows.Next() {
if err := rows.Scan(&roleName, &roleAttributes); err == nil {
fmt.Printf("%s | %s", roleName, roleAttributes)
} else {
fmt.Printf("Couldn't read row: %s", err.Error())
}
}
}

Expand Down Expand Up @@ -276,14 +283,7 @@ func seed() {
if err != nil {
die("unable to read '" + DBSeedsPath + "': " + err.Error())
}
cmd := exec.Command("psql", "-h", HostIP, "-p", HostPort, "-d", DBName, "-U", DBUser, "-e", "-v", "ON_ERROR_STOP=1")
cmd.Stdin = bytes.NewBuffer(seedsBytes)
cmd.Env = append(os.Environ(), "PGPASSWORD="+DBPassword)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't patch database w/ required data")
}
connectAndExecute(string(seedsBytes), "Couldn't seed database", false)
}

func loadSchema() {
Expand All @@ -292,16 +292,10 @@ func loadSchema() {
if err != nil {
die("unable to read '" + DBSchemaPath + "': " + err.Error())
}
cmd := exec.Command("psql", "-h", HostIP, "-p", HostPort, "-d", DBName, "-U", DBUser, "-e", "-v", "ON_ERROR_STOP=1")
cmd.Stdin = bytes.NewBuffer(schemaBytes)
cmd.Env = append(os.Environ(), "PGPASSWORD="+DBPassword)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't create database tables")
}
connectAndExecute(string(schemaBytes), "Couldn't create tables", false)
}

//TODO remove this after perlectomy
func reverseSchema() {
fmt.Fprintf(os.Stderr, "WARNING: the '%s' command will be removed with Traffic Ops Perl because it will no longer be necessary\n", CmdReverseSchema)
cmd := exec.Command("db/reverse_schema.pl")
Expand All @@ -319,14 +313,7 @@ func patch() {
if err != nil {
die("unable to read '" + DBPatchesPath + "': " + err.Error())
}
cmd := exec.Command("psql", "-h", HostIP, "-p", HostPort, "-d", DBName, "-U", DBUser, "-e", "-v", "ON_ERROR_STOP=1")
cmd.Stdin = bytes.NewBuffer(patchesBytes)
cmd.Env = append(os.Environ(), "PGPASSWORD="+DBPassword)
out, err := cmd.CombinedOutput()
fmt.Printf("%s", out)
if err != nil {
die("Can't patch database w/ required data")
}
connectAndExecute(string(patchesBytes), "Couldn't patch database", false)
}

func goose(arg string) {
Expand Down
21 changes: 21 additions & 0 deletions traffic_ops/vendor/github.com/xo/dburl/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading