Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ddl): Adding ddl command #63

Merged
merged 3 commits into from
Jun 25, 2024
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ go.work

# Ignore all SQL files
*.sql

**/bin/**
97 changes: 97 additions & 0 deletions cmd/dumper/cmd_ddl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"path/filepath"

"github.com/Jacobbrewer1/dumpster/pkg/dumpster"
"github.com/Jacobbrewer1/dumpster/pkg/logging"
"github.com/caarlos0/env/v11"
_ "github.com/go-sql-driver/mysql"
"github.com/google/subcommands"
"github.com/jmoiron/sqlx"
)

type ddlCmd struct{}

func (c *ddlCmd) Name() string {
return "ddl"
}

func (c *ddlCmd) Synopsis() string {
return "Creates a MySQL DDL of the database"
}

func (c *ddlCmd) Usage() string {
return `ddl:
Creates a MySQL DDL of the database.
`
}

func (c *ddlCmd) SetFlags(f *flag.FlagSet) {}

func (c *ddlCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
dbConnEnv := new(DatabaseConnection)
if err := env.Parse(dbConnEnv); err != nil {
slog.Error("error parsing environment variables", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

db, err := sqlx.Connect("mysql", dbConnEnv.ConnStr)
if err != nil {
slog.Error("error connecting to database", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

defer func() {
if err := db.Close(); err != nil {
slog.Warn("error closing database: %v", slog.String("error", err.Error()))
}
}()

d := dumpster.NewDumpster(db)

ddlStr, err := d.GetDDL()
if err != nil {
slog.Error("error getting DDL", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

schemaName, err := d.GetSchemaName()
if err != nil {
slog.Error("error getting schema name", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

path := fmt.Sprintf("ddl/%s.sql", schemaName)

err = os.MkdirAll(filepath.Dir(path), os.ModePerm)
if err != nil {
slog.Error("error creating directory", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

file, err := os.Create(path)
if err != nil {
slog.Error("error creating file", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

defer func() {
if err := file.Close(); err != nil {
slog.Warn("error closing file: %v", slog.String("error", err.Error()))
}
}()

_, err = file.WriteString(ddlStr)
if err != nil {
slog.Error("error writing DDL to file", slog.String("error", err.Error()))
return subcommands.ExitFailure
}

return subcommands.ExitSuccess
}
76 changes: 8 additions & 68 deletions cmd/dumper/cmd_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"database/sql"
"flag"
"fmt"
"log/slog"
Expand All @@ -13,31 +12,17 @@ import (
"github.com/Jacobbrewer1/dumpster/pkg/dataaccess"
"github.com/Jacobbrewer1/dumpster/pkg/dumpster"
"github.com/Jacobbrewer1/dumpster/pkg/logging"
"github.com/Jacobbrewer1/dumpster/pkg/vault"
"github.com/caarlos0/env/v11"
_ "github.com/go-sql-driver/mysql"
"github.com/google/subcommands"
"github.com/spf13/viper"
"github.com/jmoiron/sqlx"
"google.golang.org/api/option"
)

type dumpCmd struct {
// gcs is the bucket to upload the dump to. Setting this will enable GCS.
gcs string

// dbConnStr is the connection string to the database.
dbConnStr string

// vaultEnabled is whether to use vault for secrets.
//
// This cannot be used in tandem with the dbConnStr flag.
vaultEnabled bool

// host is the host of the database. Only used if vault is enabled.
host string

// schema is the schema of the database. Only used if vault is enabled.
schema string

// purge is the number of days to keep data for. If 0 (or not set), data will not be purged.
purge int
}
Expand All @@ -58,10 +43,6 @@ func (c *dumpCmd) Usage() string {

func (c *dumpCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.gcs, "gcs", "", "The GCS bucket to upload the dump to (Requires GCS_CREDENTIALS environment variable to be set)")
f.StringVar(&c.dbConnStr, "db-conn", "", "The connection string to the database")
f.BoolVar(&c.vaultEnabled, "vault", false, "Whether to use vault to access the database secrets (Requires VAULT_ADDR, VAULT_APPROLE_ID and VAULT_APPROLE_SECRET_ID environment variables to be set)")
f.StringVar(&c.host, "host", "", "The host of the database (Only used if vault is enabled)")
f.StringVar(&c.schema, "schema", "", "The schema of the database (Only used if vault is enabled)")
f.IntVar(&c.purge, "purge", 0, "The number of days to keep data for. If 0 (or not set), data will not be purged.")
}

Expand All @@ -72,62 +53,21 @@ func (c *dumpCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}
return subcommands.ExitFailure
}

if c.vaultEnabled && c.dbConnStr != "" {
slog.Error("cannot use vault and db-conn flags together")
f.Usage()
return subcommands.ExitUsageError
}

// Check if the database connection string is set
if c.dbConnStr == "" && !c.vaultEnabled {
slog.Error("database connection string not set")
f.Usage()
return subcommands.ExitUsageError
} else if c.vaultEnabled {
vip := viper.New()

err = vip.BindEnv("vault.addr", "VAULT_ADDR")
if err != nil {
slog.Error("error binding VAULT_ADDR environment variable", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

err = vip.BindEnv("vault.credentials_path", "VAULT_CREDENTIALS_PATH")
if err != nil {
slog.Error("error binding VAULT_CREDENTIALS_PATH environment variable", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

vc, err := vault.NewClient(vip.GetString("vault.addr"))
if err != nil {
slog.Error("error creating vault client", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

vs, err := vc.GetSecrets(vip.GetString("vault.credentials_path"))
if err != nil {
slog.Error("error getting database secrets", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

vip.Set("db.host", c.host)
vip.Set("db.schema", c.schema)

connStr := dataaccess.GenerateConnectionStr(vip, vs)
c.dbConnStr = connStr

slog.Debug("database connection setup from vault")
dbConnEnv := new(DatabaseConnection)
if err := env.Parse(dbConnEnv); err != nil {
slog.Error("error parsing environment variables", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

// Open database connection
db, err := sql.Open("mysql", c.dbConnStr)
db, err := sqlx.Open("mysql", dbConnEnv.ConnStr)
if err != nil {
slog.Error("error opening database", slog.String(logging.KeyError, err.Error()))
return subcommands.ExitFailure
}

// Close the database connection
defer func(db *sql.DB) {
defer func(db *sqlx.DB) {
if err := db.Close(); err != nil {
slog.Warn("error closing database: %v", err)
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/dumper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ package main
const (
appName = `dumpster`
)

type DatabaseConnection struct {
// ConnStr is the connection string to the database.
ConnStr string `env:"DUMPSTER_DB_CONN_STR"`
}
1 change: 1 addition & 0 deletions cmd/dumper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func main() {
subcommands.Register(subcommands.CommandsCommand(), "")

subcommands.Register(new(versionCmd), "")
subcommands.Register(new(ddlCmd), "")
subcommands.Register(new(dumpCmd), "")
subcommands.Register(new(purgeCmd), "")

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ toolchain go1.22.2

require (
cloud.google.com/go/storage v1.42.0
github.com/caarlos0/env/v11 v11.1.0
github.com/go-sql-driver/mysql v1.8.1
github.com/google/subcommands v1.2.0
github.com/hashicorp/vault/api v1.14.0
github.com/hashicorp/vault/api/auth/approle v0.7.0
github.com/jmoiron/sqlx v1.4.0
github.com/prometheus/client_golang v1.19.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand Down Expand Up @@ -133,10 +135,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
Expand All @@ -148,6 +154,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand Down
81 changes: 81 additions & 0 deletions pkg/dumpster/ddl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dumpster

import (
"bytes"
"fmt"
"text/template"
"time"
)

type ddl struct {
Database string
ServerVersion string
Tables []*table
Triggers []*trigger
CompleteTime string
}

func (d *Dumpster) GetDDL() (string, error) {
schemaName, err := d.GetSchemaName()
if err != nil {
return "", fmt.Errorf("error getting schema name: %w", err)
}

data := ddl{
Database: schemaName,
}

// Get server version
if data.ServerVersion, err = d.getServerVersion(); err != nil {
return "", fmt.Errorf("error getting server version: %w", err)
}

// Get tables
tables, err := d.getTables()
if err != nil {
return "", fmt.Errorf("error getting tables: %w", err)
}

// Get sql for each table
for _, tn := range tables {
t, err := d.createTable(tn)
if err != nil {
return "", fmt.Errorf("error creating table: %w", err)
}

t.Values = "" // For the DDL we don't need the values

data.Tables = append(data.Tables, t)
}

// Get triggers
triggers, err := d.getTriggers()
if err != nil {
return "", fmt.Errorf("error getting triggers: %w", err)
}

// Get sql for each trigger
for _, tn := range triggers {
t, err := d.createTrigger(tn)
if err != nil {
return "", fmt.Errorf("error creating trigger: %w", err)
}

data.Triggers = append(data.Triggers, t)
}

// Set complete time
data.CompleteTime = time.Now().Format(time.RFC3339)

t, err := template.New("mysqldump").Parse(tmpl)
if err != nil {
return "", fmt.Errorf("error parsing template: %w", err)
}

b := new(bytes.Buffer)
if err = t.Execute(b, data); err != nil {
return "", fmt.Errorf("error executing template: %w", err)
}

return b.String(), nil
}
8 changes: 5 additions & 3 deletions pkg/dumpster/dumpster.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package dumpster

import "database/sql"
import (
"github.com/jmoiron/sqlx"
)

type Dumpster struct {
// db is the database to dump
db *sql.DB
db *sqlx.DB
}

// NewDumpster creates a new dumpster
func NewDumpster(db *sql.DB) *Dumpster {
func NewDumpster(db *sqlx.DB) *Dumpster {
return &Dumpster{
db: db,
}
Expand Down
4 changes: 4 additions & 0 deletions vendor/github.com/caarlos0/env/v11/.gitignore

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

Loading
Loading