Skip to content

Commit

Permalink
BED-3893: stepwise migration rework (#222)
Browse files Browse the repository at this point in the history
* chore: Rework stepwise migrations
- Removed some unnecessary steps from stepwise migration process (GetFilenames, etc)
- Remodeled `Migration` and `Manifest`
- Added `Source`s to capture multiple migration sources
- Modified Migrator to hold multiple `fs.FS` sources
- Modified manifest generation to enable a multi-source migration strategy
- Reworked migration execution to run based on new multi-source Manifest model
- Improved migration tracking (no more 999.999.999 version trickery)
- Improved migration failure handling through more granular tracking and transactions
- Improved migration integration testing and added some new unit tests

* chore: bed-3893 tidying up
- Renamed instances of `Db` to `DB` for consistency
- Added documentation to migratrion rework
  • Loading branch information
sircodemane authored Nov 16, 2023
1 parent 617efad commit b6b39bc
Show file tree
Hide file tree
Showing 38 changed files with 1,145 additions and 481 deletions.
2 changes: 1 addition & 1 deletion cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func (s *BloodhoundDB) Wipe() error {
func (s *BloodhoundDB) Migrate() error {
// Run the migrator
if err := migration.NewMigrator(s.db).Migrate(); err != nil {
log.Errorf("Error during database migration phase: %v", err)
log.Errorf("Error during SQL database migration phase: %v", err)
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/api/src/database/migration/agi.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func SelectorToObjectID(rawSelector string) string {
}

func (s *Migrator) updateAssetGroups() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
var systemAssetGroups model.AssetGroups

// Lookup system asset groups
Expand Down
14 changes: 7 additions & 7 deletions cmd/api/src/database/migration/app_config.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package migration

import (
"fmt"

"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/model/appcfg"
"gorm.io/gorm"
"github.com/specterops/bloodhound/log"
)

func (s *Migrator) setAppConfigDefaults() error {
Expand All @@ -33,7 +33,7 @@ func (s *Migrator) setAppConfigDefaults() error {
}

func (s *Migrator) setFeatureFlagDefaults() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
for flagKey, availableFlag := range appcfg.AvailableFlags() {
count := int64(0)

Expand All @@ -53,7 +53,7 @@ func (s *Migrator) setFeatureFlagDefaults() error {
}

func (s *Migrator) setParameterDefaults() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
if availParams, err := appcfg.AvailableParameters(); err != nil {
return fmt.Errorf("error checking AvailableParameters: %w", err)
} else {
Expand Down
20 changes: 10 additions & 10 deletions cmd/api/src/database/migration/auth.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package migration

import (
"strings"

"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/auth"
"github.com/specterops/bloodhound/src/model"
"gorm.io/gorm"
"github.com/specterops/bloodhound/log"
)

func preload(tx *gorm.DB, associationSpecs []string) *gorm.DB {
Expand All @@ -45,7 +45,7 @@ func getAllRoles(tx *gorm.DB) (model.Roles, error) {
}

func (s *Migrator) updatePermissions() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
if existingPermissions, err := getAllPermissions(tx); err != nil {
return err
} else {
Expand All @@ -65,7 +65,7 @@ func (s *Migrator) updatePermissions() error {
}

func (s *Migrator) checkUserEmailAddresses() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
var users model.Users

for _, userAssociation := range model.UserAssociations() {
Expand Down Expand Up @@ -97,7 +97,7 @@ func (s *Migrator) checkUserEmailAddresses() error {
}

func (s *Migrator) updateRoles() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
if permissions, err := getAllPermissions(tx); err != nil {
return err
} else if existingRoles, err := getAllRoles(tx); err != nil {
Expand All @@ -122,7 +122,7 @@ func (s *Migrator) updateRoles() error {
existingRole.Name = expectedRole.Name
existingRole.Description = expectedRole.Description

if result := s.db.Save(existingRole); result.Error != nil {
if result := s.DB.Save(existingRole); result.Error != nil {
return result.Error
}

Expand All @@ -134,7 +134,7 @@ func (s *Migrator) updateRoles() error {
existingRole.Permissions = expectedRole.Permissions
existingRole.Description = expectedRole.Description

if result := s.db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(existingRole); result.Error != nil {
if result := s.DB.Session(&gorm.Session{FullSaveAssociations: true}).Updates(existingRole); result.Error != nil {
return result.Error
}

Expand Down
10 changes: 5 additions & 5 deletions cmd/api/src/database/migration/cleanup.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
// Copyright 2023 Specter Ops, Inc.
//
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// SPDX-License-Identifier: Apache-2.0

package migration

import "gorm.io/gorm"

func (s *Migrator) cleanupIngest() error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.DB.Transaction(func(tx *gorm.DB) error {
if result := tx.Exec(`truncate table ingest_tasks;`); result.Error != nil {
return result.Error
}
Expand Down
132 changes: 132 additions & 0 deletions cmd/api/src/database/migration/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2023 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package migration

import (
"io/fs"
"path/filepath"
"sort"
"strings"

"github.com/specterops/bloodhound/src/version"
)

// Manifest is a collection of available migrations. VersionTable is used to order and store
// all version of migrations contained in the manifest. Migrations is the actual
// underlying map of [version:[]migration]
type Manifest struct {
VersionTable []string
Migrations map[string][]Migration
}

// NewManifest creates a new Manifest and initializes the migrations map
func NewManifest() Manifest {
return Manifest{
Migrations: make(map[string][]Migration),
}
}

// AddMigration will add migrations to the correct location in the migrations
// map, with care to make sure the map is initialized, as well as the
// version location within the map.
func (s *Manifest) AddMigration(migration Migration) {
if s.Migrations == nil {
s.Migrations = make(map[string][]Migration)
}
if migrations, ok := s.Migrations[migration.Version.String()]; ok {
s.Migrations[migration.Version.String()] = append(migrations, migration)
} else {
s.Migrations[migration.Version.String()] = []Migration{migration}
}
}

// GenerateManifest is a wrapper around GenerateManifestAfterVersion, using
// -1.-1.-1 as the version. This ensures that a full manifest of all
// available migrations is generated. This is most useful for new installations.
func (s *Migrator) GenerateManifest() (Manifest, error) {
return s.GenerateManifestAfterVersion(version.Version{
Major: -1,
Minor: -1,
Patch: -1,
})
}

// GenerateManifestAfterVersion takes a version.Version argument and uses
// that to generate a manifest from the available migration Source's.
// It will loop through sources and build Migration's from the available
// migration files. Files labeled `schema` are considered the initial
// migration and get versioned as v0.0.0. All valid migrations that
// are versioned after the version given will be added to the manifest
// for migration. The final step is the build and sort the VersionTable
// that will be used for applying the migration in order by ExecuteMigrations.
func (s *Migrator) GenerateManifestAfterVersion(lastVersion version.Version) (Manifest, error) {
const migrationSQLFilenameSuffix = ".sql"
var manifest = NewManifest()

// loop through sources
for _, source := range s.Sources {
if dirEntries, err := fs.ReadDir(source.FileSystem, source.Directory); err != nil {
return manifest, err
} else {
// loop through file system entries
for _, entry := range dirEntries {
if !entry.IsDir() {
filename := filepath.Join(source.Directory, entry.Name())
basename := filepath.Base(filename)

// create an entry, which may or may not be added (depending on filename semantics)
var (
validMigration = false
migration = Migration{
Version: version.Version{},
Filename: filename,
Source: source.FileSystem,
}
)
if basename == "schema"+migrationSQLFilenameSuffix {
// will mark the base schema file as a valid v0.0.0 base migration
validMigration = true
} else if strings.HasPrefix(basename, version.Prefix) && strings.HasSuffix(basename, migrationSQLFilenameSuffix) {
rawVersion := strings.TrimSuffix(basename, migrationSQLFilenameSuffix)
if migrationVersion, err := version.Parse(rawVersion); err != nil {
return manifest, err
} else {
// will mark the file as a valid versioned migration
migration.Version = migrationVersion
validMigration = true
}
}
if validMigration && migration.Version.GreaterThan(lastVersion) {
manifest.AddMigration(migration)
}
}
}
}
}

// sort the versions, so we can create the version table
var versions = make([]string, 0, len(manifest.Migrations))
for ver := range manifest.Migrations {
versions = append(versions, ver)
}
sort.Slice(versions, func(a, b int) bool {
return manifest.Migrations[versions[a]][0].Version.LessThan(manifest.Migrations[versions[b]][0].Version)
})
manifest.VersionTable = versions

return manifest, nil
}
Loading

0 comments on commit b6b39bc

Please sign in to comment.