You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/cmd/commands/database/funcs.go

297 lines
8.1 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package database
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/db/common"
"github.com/hashicorp/boundary/internal/db/schema"
"github.com/hashicorp/boundary/internal/errors"
"github.com/mitchellh/cli"
)
// migrateDatabase updates the schema to the most recent version known by the binary.
// It owns the reporting to the UI any errors.
// We expect the database already to be initialized iff initialized is set to true.
// Returns a cleanup function which must be called even if an error is returned and
// an error code where a non-zero value indicates an error happened.
func migrateDatabase(ctx context.Context, ui cli.Ui, dialect, u string, initialized bool, maxOpenConns int, selectedRepairs schema.RepairMigrations) (func(), int) {
noop := func() {}
// This database is used to keep an exclusive lock on the database for the
// remainder of the command
dBase, err := common.SqlOpen(dialect, u)
if err != nil {
ui.Error(fmt.Errorf("Error establishing db connection: %w", err).Error())
return noop, 2
}
dBase.SetMaxOpenConns(maxOpenConns)
if err := dBase.PingContext(ctx); err != nil {
ui.Error(fmt.Sprintf("Unable to connect to the database at %q", u))
return noop, 2
}
man, err := schema.NewManager(ctx, schema.Dialect(dialect), dBase, schema.WithRepairMigrations(selectedRepairs))
if err != nil {
if errors.Match(errors.T(errors.MigrationLock), err) {
ui.Error("Unable to capture a lock on the database.")
} else {
ui.Error(fmt.Errorf("Error setting up schema manager: %w", err).Error())
}
return noop, 2
}
// This is an advisory lock on the DB which is released when the DB session ends.
if err := man.ExclusiveLock(ctx); err != nil {
ui.Error("Unable to capture a lock on the database.")
_ = man.Close(ctx)
return noop, 2
}
unlock := func() {
// We don't report anything since this should resolve itself anyways.
_ = man.ExclusiveUnlock(ctx)
_ = man.Close(ctx)
}
st, err := man.CurrentState(ctx)
if err != nil {
ui.Error(fmt.Errorf("Error getting database state: %w", err).Error())
return unlock, 2
}
if initialized && !st.Initialized {
ui.Output(base.WrapAtLength("Database has not been initialized. Please use 'boundary database init' to initialize the boundary database."))
return unlock, -1
}
if !initialized && st.Initialized {
ui.Output(base.WrapAtLength("Database has already been initialized. Please use 'boundary database migrate' for any upgrade needs."))
return unlock, -1
}
repairLogs, err := man.ApplyMigrations(ctx)
if err != nil {
ui.Error(fmt.Errorf("Error running database migrations: %w", err).Error())
if checkErr, ok := err.(schema.MigrationCheckError); ok {
ui.Error(fmt.Errorf("%s", strings.Join(checkErr.Problems, "\n")).Error())
ui.Error(fmt.Sprintf("To automatically repair, use 'boundary database migrate -repair=%s:%d'. This will: %s", checkErr.Edition, checkErr.Version, checkErr.RepairDescription))
}
return unlock, 2
}
if base.Format(ui) == "table" {
ui.Info("Migrations successfully run.")
}
if len(repairLogs) > 0 && base.Format(ui) == "table" {
ui.Info("Migration Repair logs...")
for _, e := range repairLogs {
ui.Info(fmt.Sprintf("%s:%d:", e.Edition, e.Version))
for _, entry := range e.Entry {
ui.Info(entry)
}
}
}
logs, err := man.GetMigrationLog(ctx)
if err != nil {
ui.Error(fmt.Errorf("Error retrieving database migration logs: %w", err).Error())
return unlock, 2
}
if len(logs) > 0 && base.Format(ui) == "table" {
ui.Info("Migration Logs...")
for _, e := range logs {
ui.Info(e.Entry)
}
}
return unlock, 0
}
type RoleInfo struct {
RoleId string `json:"scope_id"`
Name string `json:"name"`
}
func generateInitialLoginRoleTableOutput(in *RoleInfo) string {
nonAttributeMap := map[string]any{
"Role ID": in.RoleId,
"Name": in.Name,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
"Initial login role information:",
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}
func generateInitialAuthenticatedUserRoleOutput(in *RoleInfo) string {
nonAttributeMap := map[string]any{
"Role ID": in.RoleId,
"Name": in.Name,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
"Initial authenticated user role information:",
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}
type AuthInfo struct {
AuthMethodId string `json:"auth_method_id"`
AuthMethodName string `json:"auth_method_name"`
LoginName string `json:"login_name"`
Password string `json:"password"`
ScopeId string `json:"scope_id"`
UserId string `json:"user_id"`
UserName string `json:"user_name"`
}
func generateInitialAuthTableOutput(in *AuthInfo) string {
nonAttributeMap := map[string]any{
"Scope ID": in.ScopeId,
"Auth Method ID": in.AuthMethodId,
"Auth Method Name": in.AuthMethodName,
"Login Name": in.LoginName,
"Password": in.Password,
"User ID": in.UserId,
"User Name": in.UserName,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
"Initial auth information:",
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}
type ScopeInfo struct {
ScopeId string `json:"scope_id"`
Type string `json:"type"`
Name string `json:"name"`
}
func generateInitialScopeTableOutput(in *ScopeInfo) string {
nonAttributeMap := map[string]any{
"Scope ID": in.ScopeId,
"Type": in.Type,
"Name": in.Name,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
fmt.Sprintf("Initial %s scope information:", in.Type),
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}
type HostInfo struct {
HostCatalogId string `json:"host_catalog_id"`
HostSetId string `json:"host_set_id"`
HostId string `json:"host_id"`
Type string `json:"type"`
ScopeId string `json:"scope_id"`
HostCatalogName string `json:"host_catalog_name"`
HostSetName string `json:"host_set_name"`
HostName string `json:"host_name"`
}
func generateInitialHostResourcesTableOutput(in *HostInfo) string {
nonAttributeMap := map[string]any{
"Host Catalog ID": in.HostCatalogId,
"Host Catalog Name": in.HostCatalogName,
"Host Set ID": in.HostSetId,
"Host Set Name": in.HostSetName,
"Host ID": in.HostId,
"Host Name": in.HostName,
"Type": in.Type,
"Scope ID": in.ScopeId,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
"Initial host resources information:",
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}
type TargetInfo struct {
TargetId string `json:"target_id"`
DefaultPort uint32 `json:"default_port"`
SessionMaxSeconds uint32 `json:"session_max_seconds"`
SessionConnectionLimit int32 `json:"session_connection_limit"`
Type string `json:"type"`
ScopeId string `json:"scope_id"`
Name string `json:"name"`
}
func generateInitialTargetTableOutput(in *TargetInfo) string {
nonAttributeMap := map[string]any{
"Target ID": in.TargetId,
"Default Port": in.DefaultPort,
"Session Max Seconds": in.SessionMaxSeconds,
"Session Connection Limit": in.SessionConnectionLimit,
"Type": in.Type,
"Scope ID": in.ScopeId,
"Name": in.Name,
}
maxLength := 0
for k := range nonAttributeMap {
if len(k) > maxLength {
maxLength = len(k)
}
}
ret := []string{
"",
"Initial target information:",
base.WrapMap(2, maxLength+2, nonAttributeMap),
}
return base.WrapForHelpText(ret)
}