diff --git a/app.go b/app.go index ab98c5e..24e42e2 100644 --- a/app.go +++ b/app.go @@ -90,12 +90,11 @@ func migrateSpinnakerApplication() error { return nil } - payload := map[string][]map[string]interface{}{"pipelines": pipelines} - - stages, _ := getSupportedStages() - if stages != nil { - checkUnsupportedStages(payload, stages) + dryRun := migrationReq.DryRun + payload := map[string]interface{}{ + "pipelines": pipelines, // Expecting pipelines as []map[string]interface{} + "dryRun": dryRun, // dryRun as a bool } - _, err = createSpinnakerPipelines(payload) + _, err = createSpinnakerPipelines(payload, dryRun) return err } diff --git a/main.go b/main.go index 08fddd8..43dc71e 100644 --- a/main.go +++ b/main.go @@ -387,6 +387,11 @@ func main() { Usage: "x509 key location for authenticating with spinnaker", Destination: &migrationReq.Key, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "dryRun", + Usage: "perform a dry run without side effects", + Destination: &migrationReq.DryRun, + }), } app := &cli.App{ Name: "harness-upgrade", @@ -587,6 +592,11 @@ func main() { Name: "pipelines", Usage: "Import pipelines into an existing project by providing the `appId` & `pipelineIds`", Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "dryRun", + Usage: "dry run", + Destination: &migrationReq.DryRun, + }, &cli.BoolFlag{ Name: "all", Usage: "all pipelines", @@ -619,7 +629,7 @@ func main() { }, &cli.BoolFlag{ Name: "insecure", - Usage: "Weteher to validate the TLS certificate or not", + Usage: "Whether to validate the TLS certificate or not", Destination: &migrationReq.AllowInsecureReq, }, &cli.StringFlag{ diff --git a/pipelines.go b/pipelines.go index c02186d..31f7963 100644 --- a/pipelines.go +++ b/pipelines.go @@ -126,19 +126,17 @@ func migrateSpinnakerPipelines() error { if err != nil { return err } - - payload := map[string][]map[string]interface{}{"pipelines": pipelines} - - stages, _ := getSupportedStages() - if stages != nil { - checkUnsupportedStages(payload, stages) + dryRun := migrationReq.DryRun + payload := map[string]interface{}{ + "pipelines": pipelines, // Expecting pipelines as []map[string]interface{} + "dryRun": dryRun, // dryRun as a bool } - _, err = createSpinnakerPipelines(payload) + _, err = createSpinnakerPipelines(payload, dryRun) return err } -func checkUnsupportedStages(payload map[string][]map[string]interface{}, supportedStages []string) { +func checkUnsupportedStages(payload map[string]interface{}, supportedStages []string) { unsupportedStagesMap := make(map[string][]string) // Convert supported stages to lowercase for case-insensitive comparison @@ -151,7 +149,7 @@ func checkUnsupportedStages(payload map[string][]map[string]interface{}, support re := regexp.MustCompile(`[^a-zA-Z0-9]+`) // Iterate over each pipeline - for _, pipeline := range payload["pipelines"] { + for _, pipeline := range payload["pipelines"].([]map[string]interface{}) { unsupportedStages := []string{} // Get stages from the pipeline @@ -180,14 +178,19 @@ func checkUnsupportedStages(payload map[string][]map[string]interface{}, support } } - // Log unsupported stages if len(unsupportedStagesMap) > 0 { for pipelineID, stages := range unsupportedStagesMap { - log.Warnf("Pipeline ID: %s, Unsupported Stages: %v", pipelineID, stages) + // Construct a single log message for each pipeline + message := fmt.Sprintf("\n Pipeline with id: %s\n has unsupported Stages:\n", pipelineID) + for _, stage := range stages { + message += fmt.Sprintf(" - %s\n", stage) + } + log.Warn(message) } } else { log.Info("All stages in all pipelines are supported.") } + } func fetchDependentPipelines(pipelines []map[string]interface{}, err error, authMethod string) ([]map[string]interface{}, error) { @@ -472,16 +475,19 @@ func getSupportedStages() ([]string, error) { return stages, nil } -func createSpinnakerPipelines(pipelines interface{}) (reqId string, err error) { +func createSpinnakerPipelines(pipelines map[string]interface{}, dryRun bool) (reqId string, err error) { queryParams := map[string]string{ ProjectIdentifier: migrationReq.ProjectIdentifier, OrgIdentifier: migrationReq.OrgIdentifier, AccountIdentifier: migrationReq.Account, } - err = CheckProjectExistsAndCreate() - if err != nil { - return "", err + if !dryRun && migrationReq.Environment != Dev { + err = CheckProjectExistsAndCreate() + if err != nil { + return "", err + } } + j, err := json.MarshalIndent(pipelines, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal pipelines JSON: %v", err) @@ -497,16 +503,9 @@ func createSpinnakerPipelines(pipelines interface{}) (reqId string, err error) { if err != nil { return "", fmt.Errorf("failed to get resource: %v", err) } - if resource.Errors != nil && len(resource.Errors) > 0 { - // Convert the data to JSON - jsonData, err := json.MarshalIndent(resource.Errors, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal resource errors JSON: %v", err) - } - // Convert bytes to string and print - jsonString := string(jsonData) - log.Warnf(jsonString) - return "", fmt.Errorf("failed to create pipeline : %v", migrationReq.PipelineName) + hasErrors := resource.Errors != nil && len(resource.Errors) > 0 + if hasErrors { + printAllErrors(pipelines, resource.Errors) } if len(resource.RequestId) != 0 { reqId = resource.RequestId @@ -520,7 +519,42 @@ func createSpinnakerPipelines(pipelines interface{}) (reqId string, err error) { jsonString := string(jsonData) log.Warnf("Entity not migrated : %s", jsonString) } - reconcilePipeline(resp, queryParams) - log.Info("Spinnaker migration completed") + // Pretty print successfullyMigratedDetails + if len(resource.SuccessfullyMigratedDetails) > 0 { + printCreatedEntities(resource.SuccessfullyMigratedDetails) + } else { + return "", fmt.Errorf("spinnaker migration failed") + } + if !dryRun && migrationReq.Environment != Dev { + reconcilePipeline(resp, queryParams) + log.Info("Spinnaker migration completed") + } else { + log.Infof("Note: This was a dry run of the spinnaker migration") + } + return reqId, nil } +func printAllErrors(pipelines map[string]interface{}, errors []UpgradeError) { + stages, _ := getSupportedStages() + if stages != nil { + checkUnsupportedStages(pipelines, stages) + } + printResourceErrors(errors) +} + +func printResourceErrors(errors []UpgradeError) error { + for _, err := range errors { + if !strings.Contains(err.Message, "SpinnakerStageType") { + log.Warnf(fmt.Sprintf(" . %s\n", err.Message)) + } + } + return nil +} + +func printCreatedEntities(resources []SuccessfullyMigratedDetail) { + for _, detail := range resources { + ngDetail := detail.NgEntityDetail + log.Printf("created entity:\n EntityType: %s\n Identifier: %s\n OrgIdentifier: %s\n ProjectIdentifier: %s\n", + ngDetail.EntityType, ngDetail.Identifier, ngDetail.OrgIdentifier, ngDetail.ProjectIdentifier) + } +} diff --git a/types.go b/types.go index aee6330..5ab528d 100644 --- a/types.go +++ b/types.go @@ -191,12 +191,25 @@ type MigrationStats struct { } type Resource struct { - RequestId string `json:"requestId"` - Stats map[string]MigrationStats `json:"stats"` - Errors []UpgradeError `json:"errors"` - Status string `json:"status"` - ResponsePayload interface{} `json:"responsePayload"` - SkipDetails []NGSkipDetail `json:"skipDetails"` + RequestId string `json:"requestId"` + Stats map[string]MigrationStats `json:"stats"` + Errors []UpgradeError `json:"errors"` + Status string `json:"status"` + ResponsePayload interface{} `json:"responsePayload"` + SkipDetails []NGSkipDetail `json:"skipDetails"` + SuccessfullyMigratedDetails []SuccessfullyMigratedDetail `json:"successfullyMigratedDetails"` +} + +type SuccessfullyMigratedDetail struct { + CgEntityDetail interface{} `json:"cgEntityDetail"` // Assuming cgEntityDetail can be nil or of a specific type + NgEntityDetail NgEntityDetail `json:"ngEntityDetail"` +} + +type NgEntityDetail struct { + EntityType string `json:"entityType"` + Identifier string `json:"identifier"` + OrgIdentifier string `json:"orgIdentifier"` + ProjectIdentifier string `json:"projectIdentifier"` } type ResponseBody struct {