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: new standalone lineage table + associated migration #173

Merged
merged 5 commits into from Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 82 additions & 7 deletions db/db.go
Expand Up @@ -2,6 +2,7 @@ package db

import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
Expand Down Expand Up @@ -55,6 +56,7 @@ func Init(config config.DBConfig, debug bool) *Database {
&types.Resource{},
&types.Attribute{},
&types.OutputValue{},
&types.Lineage{},
&types.Plan{},
&types.PlanModel{},
&types.PlanModelVariable{},
Expand All @@ -76,20 +78,70 @@ func Init(config config.DBConfig, debug bool) *Database {
if debug {
db.Config.Logger.LogMode(logger.Info)
}
return &Database{db}

d := &Database{db}
if err = d.MigrateLineage(); err != nil {
log.Fatalf("Lineage migration failed: %v\n", err)
}

return d
}

// MigrateLineage is a migration function to update db and its data to the
// new lineage db scheme. It will update State table data, delete "lineage" column
// and add corresponding Lineage entries
func (db *Database) MigrateLineage() error {
if db.Migrator().HasColumn(&types.State{}, "lineage") {
states := db.ListStates()
for _, stPath := range states {
// Recover State from db for update
var st types.State
res := db.First(&st, types.State{Path: stPath})
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
return fmt.Errorf("State not found in db")
}

if err := db.UpdateState(st); err != nil {
return fmt.Errorf("Failed to update %s state during lineage migration: %v", stPath, err)
}
}

// Custom migration rules
if err := db.Migrator().DropColumn(&types.State{}, "lineage"); err != nil {
return fmt.Errorf("Failed to drop lineage column during migration: %v", err)
}
}

return nil
}

type attributeValues map[string]interface{}

func (db *Database) stateS3toDB(sf *statefile.File, path string, versionID string) (st types.State) {
var version types.Version
db.First(&version, types.Version{VersionID: versionID})
st = types.State{
Path: path,
Version: version,
TFVersion: sf.TerraformVersion.String(),
Serial: int64(sf.Serial),
Lineage: sf.Lineage,

// Check if the associated lineage is already present in lineages table
// If so, it recovers its ID otherwise it inserts it at the same time as the state
var lineage types.Lineage
if errors.Is(db.First(&lineage, types.Lineage{Value: sf.Lineage}).Error, gorm.ErrRecordNotFound) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's just me, but I find this line a bit loaded. Can we split into:

Suggested change
if errors.Is(db.First(&lineage, types.Lineage{Value: sf.Lineage}).Error, gorm.ErrRecordNotFound) {
err := db.First(&lineage, types.Lineage{Value: sf.Lineage}).Error
if errors.Is(err, gorm.ErrRecordNotFound) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Arguably, this should never happen this the migration is done when Terraboard is launched. Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably, this should never happen this the migration is done when Terraboard is launched. Or am I missing something?

Precisely, this condition was introduced by the arrival of the new lineage table. Indeed, when we insert a state there are two possible scenarios:

  • The associated lineage was already inserted in the lineages table previously by another state. So, we just recover the lineage id and set the foreign key directly.
  • The lineage isn't in the lineages table, so we set the lineage structure in the state one so that gorm takes care of inserting it at the same time as the state

st = types.State{
Path: path,
Version: version,
TFVersion: sf.TerraformVersion.String(),
Serial: int64(sf.Serial),
Lineage: types.Lineage{
Value: sf.Lineage,
},
}
} else {
st = types.State{
Path: path,
Version: version,
TFVersion: sf.TerraformVersion.String(),
Serial: int64(sf.Serial),
LineageID: lineage.ID,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if there's an error but it's not gorm.ErrRecordNotFound?

Copy link
Member Author

Choose a reason for hiding this comment

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

On the Gorm.v2 documentation, they only evoke this error for db.First, so I started from the principle that there could be no other ... An interpretation that may be wrong, however, what do you think?
By the way, putting a more generic error handling here will not cost anything and will remove the doubt 🧐

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it'd be safer to have 3 cases:

  • err == nil
  • err is gorm.ErrRecordNotFound
  • else raise an Unknown error

}

for _, m := range sf.State.Modules {
Expand Down Expand Up @@ -169,6 +221,29 @@ func (db *Database) InsertState(path string, versionID string, sf *statefile.Fil
return nil
}

// UpdateState update a Terraform State in the Database with Lineage foreign constraint
// It will also insert Lineage entry in the db if needed
func (db *Database) UpdateState(st types.State) error {
raphink marked this conversation as resolved.
Show resolved Hide resolved
// Get lineage from old column
if err := db.Raw("SELECT lineage FROM states WHERE path = ?", st.Path).Scan(&st.Lineage.Value).Error; err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there a way do do that using the Go structs?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not to my knowledge, knowing that it is a request to recover an element which is no longer present in the structure during the migration.
There is however the MigrateColomn method of Gorm's Migrator interface (https://gorm.io/docs/migration.html) but I'm not sure what it does, I didn't understand the source code and there is no documentation...

return fmt.Errorf("Error on %s lineage recovering during migration: %v", st.Path, err)
}

// Create Lineage entry if not exist (value column is unique)
db.Create(&st.Lineage)

// Get Lineage ID for foreign constraint
var lineage types.Lineage
res := db.First(&lineage, lineage)
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
return fmt.Errorf("State's lineage not found in db during update")
}
st.LineageID = lineage.ID
st.Lineage = lineage

return db.Save(&st).Error
}

// InsertVersion inserts an AWS S3 Version in the Database
func (db *Database) InsertVersion(version *state.Version) error {
var v types.Version
Expand Down
14 changes: 11 additions & 3 deletions types/db.go
Expand Up @@ -29,8 +29,16 @@ type State struct {
VersionID sql.NullInt64 `gorm:"index" json:"-"`
TFVersion string `gorm:"varchar(10)" json:"terraform_version"`
Serial int64 `json:"serial"`
Lineage string `json:"lineage"`
Modules []Module `json:"modules"`
LineageID uint `gorm:"index" json:"-"`
raphink marked this conversation as resolved.
Show resolved Hide resolved
Lineage Lineage
Modules []Module `json:"modules"`
}

type Lineage struct {
gorm.Model
Value string `gorm:"index;unique" json:"lineage"`
States []State `json:"states"`
Plans []Plan `json:"plans"`
}

// Module is a Terraform module in a State
Expand Down Expand Up @@ -72,7 +80,7 @@ type Attribute struct {
// Plan is a Terraform plan
type Plan struct {
gorm.Model
Lineage string `json:"lineage"`
LineageID uint `gorm:"index" json:"-"`
TFVersion string `gorm:"varchar(10)" json:"terraform_version"`
GitRemote string `json:"git_remote"`
GitCommit string `gorm:"varchar(50)" json:"git_commit"`
Expand Down