cmd/assessment.go (146 lines of code) (raw):

/* Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // 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.*/ package cmd import ( "context" "flag" "fmt" "os" "path" "path/filepath" "time" "github.com/GoogleCloudPlatform/spanner-migration-tool/assessment" "github.com/GoogleCloudPlatform/spanner-migration-tool/common/constants" "github.com/GoogleCloudPlatform/spanner-migration-tool/common/utils" "github.com/GoogleCloudPlatform/spanner-migration-tool/conversion" "github.com/GoogleCloudPlatform/spanner-migration-tool/expressions_api" "github.com/GoogleCloudPlatform/spanner-migration-tool/internal" "github.com/GoogleCloudPlatform/spanner-migration-tool/logger" "github.com/GoogleCloudPlatform/spanner-migration-tool/profiles" "github.com/GoogleCloudPlatform/spanner-migration-tool/sources/common" "github.com/google/subcommands" "go.uber.org/zap" ) // AssessmentCmd struct with flags. type AssessmentCmd struct { source string sourceProfile string target string targetProfile string assessmentProfile string project string logLevel string dryRun bool validate bool sessionJSON string } // Name returns the name of operation. func (cmd *AssessmentCmd) Name() string { return "assessment" } // Synopsis returns summary of operation. func (cmd *AssessmentCmd) Synopsis() string { return "generate assessment for migration of the current database to Spanner" } // Usage returns usage info of the command. func (cmd *AssessmentCmd) Usage() string { return fmt.Sprintf(`%v assessment -source=[source] -source-profile="key1=value1,key2=value2" -assessment-profile="key1=value1" ... Run an assessment on the existing source db and create a report on the complexity of performing a migration to Spanner. The configuration of the assessment collectors is provided in the assessment-profile The assessment flags are: `, path.Base(os.Args[0])) } // SetFlags sets the flags. func (cmd *AssessmentCmd) SetFlags(f *flag.FlagSet) { f.StringVar(&cmd.source, "source", "", "Flag for specifying source DB, (e.g., `PostgreSQL`, `MySQL`, `DynamoDB`)") f.StringVar(&cmd.sourceProfile, "source-profile", "", "Flag for specifying connection profile for source database e.g., \"file=<path>,format=dump\"") f.StringVar(&cmd.target, "target", "Spanner", "Specifies the target DB, defaults to Spanner (accepted values: `Spanner`)") f.StringVar(&cmd.targetProfile, "target-profile", "", "Flag for specifying connection profile for target database e.g., \"dialect=postgresql\"") f.StringVar(&cmd.assessmentProfile, "assessment-profile", "", "File for specifying configuration to be used during assessment. e.g. \"app-code-location=\"<a/b/c>") f.StringVar(&cmd.project, "project", "", "Flag specifying default project id for all the generated resources for the migration") f.StringVar(&cmd.logLevel, "log-level", "INFO", "Configure the logging level for the command (INFO, DEBUG), defaults to INFO") f.BoolVar(&cmd.dryRun, "dry-run", false, "Flag for generating DDL and schema conversion report without creating a spanner database") f.BoolVar(&cmd.validate, "validate", false, "Flag for validating if all the required input parameters are present") f.StringVar(&cmd.sessionJSON, "session", "", "Optional. Specifies the file we restore session state from.") } func (cmd *AssessmentCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { // Cleanup smt tmp data directory in case residuals remain from prev runs. os.RemoveAll(filepath.Join(os.TempDir(), constants.SMT_TMP_DIR)) var err error defer func() { if err != nil { logger.Log.Fatal("FATAL error", zap.Error(err)) } }() err = logger.InitializeLogger(cmd.logLevel) if err != nil { fmt.Println("Error initialising logger, did you specify a valid log-level? [DEBUG, INFO, WARN, ERROR, FATAL]", err) return subcommands.ExitFailure } defer logger.Log.Sync() // Generate source and spanner schema // Initialize collectors based on assessment profile // Initialize the assessment engine with the collectors and schema // Generate assessment report if cmd.validate { return subcommands.ExitSuccess } conv, sourceProfile, exitStatus := generateConv(cmd) if conv == nil { return exitStatus } assessmentConfigMap, err := profiles.ParseMap(cmd.assessmentProfile) if err != nil { logger.Log.Fatal("could not parse assessment profile", zap.Error(err)) return subcommands.ExitFailure } assessmentOutput, err := assessment.PerformAssessment(conv, sourceProfile, assessmentConfigMap, cmd.project) if err != nil { logger.Log.Fatal("could not complete assessment", zap.Error(err)) return subcommands.ExitFailure } getInfo := utils.GetUtilInfoImpl{} dbName, err := getInfo.GetDatabaseName(sourceProfile.Driver, time.Now()) if err != nil { err = fmt.Errorf("can't generate database name for prefix: %v", err) return subcommands.ExitFailure } assessment.GenerateReport(dbName, assessmentOutput) // Follow up if required - save assessment report // Cleanup smt tmp data directory. os.RemoveAll(filepath.Join(os.TempDir(), constants.SMT_TMP_DIR)) return subcommands.ExitSuccess } func generateConv(cmd *AssessmentCmd) (*internal.Conv, profiles.SourceProfile, subcommands.ExitStatus) { sourceProfile, targetProfile, ioHelper, _, err := PrepareMigrationPrerequisites(cmd.sourceProfile, cmd.targetProfile, cmd.source) if err != nil { err = fmt.Errorf("error while preparing prerequisites for migration: %v", err) return nil, profiles.SourceProfile{}, subcommands.ExitUsageError } var conv *internal.Conv convImpl := &conversion.ConvImpl{} if cmd.sessionJSON != "" { logger.Log.Info("Loading the conversion context from session file."+ " The source profile will not be used for the schema conversion.", zap.String("sessionFile", cmd.sessionJSON)) conv = internal.MakeConv() err = conversion.ReadSessionFile(conv, cmd.sessionJSON) if err != nil { return nil, profiles.SourceProfile{}, subcommands.ExitFailure } expressionVerificationAccessor, _ := expressions_api.NewExpressionVerificationAccessorImpl(context.Background(), targetProfile.Conn.Sp.Project, targetProfile.Conn.Sp.Instance) schemaToSpanner := common.SchemaToSpannerImpl{ ExpressionVerificationAccessor: expressionVerificationAccessor, } err := schemaToSpanner.VerifyExpressions(conv) if err != nil { return nil, profiles.SourceProfile{}, subcommands.ExitFailure } } else { ctx := context.Background() ddlVerifier, err := expressions_api.NewDDLVerifierImpl(ctx, "", "") if err != nil { logger.Log.Error(fmt.Sprintf("error trying create ddl verifier: %v", err)) return nil, profiles.SourceProfile{}, subcommands.ExitFailure } sfs := &conversion.SchemaFromSourceImpl{ DdlVerifier: ddlVerifier, } conv, err = convImpl.SchemaConv(cmd.project, sourceProfile, targetProfile, &ioHelper, sfs) if err != nil { return nil, profiles.SourceProfile{}, subcommands.ExitFailure } } if conv == nil { logger.Log.Error("Could not initialize conversion context") return nil, profiles.SourceProfile{}, subcommands.ExitFailure } logger.Log.Info("completed creation on source and spanner schema") return conv, sourceProfile, 0 }