diff --git a/Readme.md b/Readme.md index 67f7142..116538b 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,7 @@ # Harness Upgrade CLI to help customers, CSMs and developers with migrating their current gen harness account to next gen -## Getting Started - -### Installation +## Installation Download the latest release from GitHub releases. We support MacOS(`darwim & amd64`), Linux(`linux + (amd64/arm64)`) and Windows(`windows+amd64`). Please download the right assets. Extract the file anywhere. We recommend that you move it to a folder that is specified in your path. @@ -15,11 +13,12 @@ harness-upgrade help If you are using macOS then just do ```shell mv harness-upgrade /usr/local/bin/ +harness-upgrade help ``` If the above works successfully you should see all the commands that are supported with `harness-upgrade` -### Migrating using the step-by-step guide +## Migrating using the step-by-step guide To migrate account level entities such as secret managers, secrets & connectors ```shell @@ -36,6 +35,11 @@ To migrate workflows harness-upgrade workflows ``` +To migrate pipelines +```shell +harness-upgrade pipelines +``` + We use API keys created in NextGen to make API calls. The token can be provided in the step-by-step guide in the prompt or as below ```shell @@ -48,41 +52,75 @@ export HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade ``` -### Migrating with a single command -To migrate all account level entities +OR +```shell +harness-upgrade --api-key apiKey +``` + +## Migrating with a single command +Using the step-by-step guide is the recommended way to get started with upgrade, but filling the prompts everytime can be tedious. If you wish to provide all or a few inputs you can pass them using the flags. If required arguments are not provided we will prompt for the inputs. + +### To migrate all account level entities ```shell HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --env ENV ``` -To migrate an application +### To migrate an application ```shell -HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade app --app APP_ID --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --env ENV +HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade --app APP_ID --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --env ENV app ``` -To migrate workflows +### To migrate workflows ```shell -HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade workflows --app APP_ID --workflows WORKFLOW_IDS --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --workflow-scope SCOPE --env ENV +HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade --app APP_ID --workflows WORKFLOW_IDS --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --workflow-scope SCOPE --env ENV workflows +``` + +### To migrate pipelines + +```shell +HARNESS_MIGRATOR_AUTH=apiKey harness-upgrade --app APP_ID --pipelines PIPELINE_IDS --project PROJECT --org ORG --account ACCOUNT_ID --secret-scope SCOPE --connector-scope SCOPE --template-scope SCOPE --workflow-scope SCOPE --env ENV pipelines +``` + +## Migrating by providing the flags from a file +If you wish to provide the flags from a file you can use the `--load` to load flags from a file. You can find templates for various options in the `templates/` directory. + +```shell +# To migrate the account level entities +harness-upgrade --load 'templates/account.yaml' + +# To migrate the app +harness-upgrade app --load 'templates/app.yaml' + +# To migrate the workflows +harness-upgrade workflows --load 'templates/workflows.yaml' + +# To migrate the pipelines +harness-upgrade pipelines --load 'templates/pipelines.yaml' ``` -| Flag | Details | -|-------------------|----------------------------------------------------------------------------------------------------| -| --env | Your target environment. It can be either `Dev`, `QA`, `Prod` or `Prod3` | -| --account | ID of the account that you wish to migrate | -| --secret-scope | Scope at which the secret has to be created. It can be `project`, `org` or `account` | -| --connector-scope | Scope at which the connector has to be created. It can be `project`, `org` or `account` | -| --template-scope | Scope at which the templates has to be created. It can be `project`, `org` or `account` | -| --workflow-scope | Scope at which the workflow as template has to be created. It can be `project`, `org` or `account` | -| --org | Identifier of the target org | -| --project | Identifier of the target project | -| --app | Application ID from current gen | -| --workflows | Workflow Ids as comma separated values(ex. workflow1,workflow2,workflow3) | -| --debug | If debug level logs need to be printed | -| --json | Formatted the logs as JSON | +## All the Flags + +| Flag | Details | +|-------------------|------------------------------------------------------------------------------------------------------------------------| +| --env | Your target environment. It can be either `Dev`, `QA`, `Prod` or `Prod3` | +| --account | `ACCOUNT_ID` of the account that you wish to migrate | +| --api-key | `API_KEY` to authenticate & authorise the migration. You may also use the `HARNESS_MIGRATOR_AUTH` env variable instead | +| --secret-scope | Scope at which the secret has to be created. It can be `project`, `org` or `account` | +| --connector-scope | Scope at which the connector has to be created. It can be `project`, `org` or `account` | +| --template-scope | Scope at which the templates has to be created. It can be `project`, `org` or `account` | +| --workflow-scope | Scope at which the workflow as template has to be created. It can be `project`, `org` or `account` | +| --org | Identifier of the target org | +| --project | Identifier of the target project | +| --app | Application ID from current gen | +| --workflows | Workflow Ids as comma separated values(ex. `workflow1,workflow2,workflow3`) | +| --pipelines | Pipeline Ids as comma separated values(ex. `pipeline1,pipeline2,pipeline3`) | +| --debug | If debug level logs need to be printed | +| --json | Formatted the logs as JSON | If not all the required flags are provided we will fall back to prompt based technique to capture all the required details. ## Contact -If you face any issues please reach out on the maintainer(s): Deepak Parthurya, Brett Zane, Rohan Gupta +If you face any issues please reach out to us or feel free to create a GitHub issue. diff --git a/account.go b/account.go new file mode 100644 index 0000000..5792cc2 --- /dev/null +++ b/account.go @@ -0,0 +1,49 @@ +package main + +import ( + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "os" +) + +func migrateAccountLevelEntities(*cli.Context) error { + log.Info("Migrating all account level entities like secret managers, secrets, connectors.") + promptConfirm := PromptDefaultInputs() + // Based on the scopes of entities determine the destination details + promptConfirm = PromptOrgAndProject([]string{migrationReq.SecretScope, migrationReq.ConnectorScope}) || promptConfirm + logMigrationDetails() + + // We confirm if they wish to proceed or not + if promptConfirm { + confirm := ConfirmInput("Do you wish to proceed importing all secret managers, secrets & connectors?") + if !confirm { + os.Exit(1) + } + } + + // Finally Make the API calls to create all entities + url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) + + // Create Secret Managers + log.Info("Importing all secret managers from CG to NG...") + CreateEntity(url, migrationReq.Auth, getReqBody(SecretManager, Filter{ + Type: All, + })) + log.Info("Imported all secret managers.") + + // Create Secrets + log.Info("Importing all secrets from CG to NG...") + CreateEntity(url, migrationReq.Auth, getReqBody(Secret, Filter{ + Type: All, + })) + log.Info("Imported all secrets.") + + // Create Connectors + log.Info("Importing all connectors from CG to NG....") + CreateEntity(url, migrationReq.Auth, getReqBody(Connector, Filter{ + Type: All, + })) + log.Info("Imported all connectors.") + + return nil +} diff --git a/app.go b/app.go new file mode 100644 index 0000000..196608a --- /dev/null +++ b/app.go @@ -0,0 +1,35 @@ +package main + +import ( + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +func migrateApp(*cli.Context) error { + promptConfirm := PromptDefaultInputs() + if len(migrationReq.AppId) == 0 { + promptConfirm = true + migrationReq.AppId = TextInput("Please provide the application ID of the app that you wish to import -") + } + + promptConfirm = PromptOrgAndProject([]string{migrationReq.SecretScope, migrationReq.ConnectorScope, migrationReq.TemplateScope}) || promptConfirm + + logMigrationDetails() + + if promptConfirm { + confirm := ConfirmInput("Do you want to proceed with app migration?") + if !confirm { + log.Fatal("Aborting...") + } + } + + url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) + // Migrating the app + log.Info("Importing the application....") + CreateEntity(url, migrationReq.Auth, getReqBody(Application, Filter{ + AppId: migrationReq.AppId, + })) + log.Info("Imported the application.") + + return nil +} diff --git a/constants.go b/constants.go index f2baae4..649943d 100644 --- a/constants.go +++ b/constants.go @@ -6,6 +6,8 @@ const ( Connector = "CONNECTOR" Application = "APPLICATION" Workflow = "WORKFLOW" + Pipeline = "PIPELINE" + Template = "TEMPLATE" ) const ( diff --git a/go.mod b/go.mod index e798cb4..7d8ec0c 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.6 github.com/sirupsen/logrus v1.9.0 github.com/urfave/cli/v2 v2.23.6 + golang.org/x/exp v0.0.0-20230111222715-75897c7a292a ) require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.2 // indirect @@ -16,7 +18,8 @@ require ( github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect golang.org/x/text v0.3.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e972acd..d3a5243 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -33,16 +35,21 @@ github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4= github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9IppkcT72GKnWjNf5W8= +golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper.go b/helper.go index cc102db..4fa6cd0 100644 --- a/helper.go +++ b/helper.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" log "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" "io" "net/http" "os" @@ -137,3 +138,13 @@ func getOrDefault(value string, defaultValue string) string { } return value } + +func ContainsAny[E comparable](source []E, values []E) bool { + for i := range values { + v := values[i] + if slices.Contains(source, v) { + return true + } + } + return false +} diff --git a/main.go b/main.go index 062c084..bd3f0cc 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,11 @@ package main import ( - "crypto/tls" "fmt" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "net/http" + "github.com/urfave/cli/v2/altsrc" "os" - "strings" ) var Version = "development" @@ -27,6 +25,8 @@ var migrationReq = struct { ProjectIdentifier string `survey:"project"` AppId string `survey:"appId"` WorkflowIds string `survey:"workflowIds"` + PipelineIds string `survey:"pipelineIds"` + File string `survey:"load"` Debug bool `survey:"debug"` Json bool `survey:"json"` AllowInsecureReq bool `survey:"insecure"` @@ -46,133 +46,18 @@ func getReqBody(entityType EntityType, filter Filter) RequestBody { return RequestBody{Inputs: inputs, DestinationDetails: destination, EntityType: entityType, Filter: filter} } -func PromptDefaultInputs() bool { - promptConfirm := false - - if len(migrationReq.Environment) == 0 { - promptConfirm = true - migrationReq.Environment = SelectInput("Which environment?", []string{Dev, QA, Prod, Prod3}, Dev) - } - - // Check if auth is provided. If not provided then request for one - migrationReq.Auth = os.Getenv("HARNESS_MIGRATOR_AUTH") - if len(migrationReq.Auth) == 0 { - migrationReq.Auth = TextInput("The environment variable 'HARNESS_MIGRATOR_AUTH' is not set. What is the api key?") - } - - if migrationReq.Environment == "Dev" || migrationReq.AllowInsecureReq { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - - if len(migrationReq.Account) == 0 { - promptConfirm = true - migrationReq.Account = TextInput("Account that you wish to migrate:") - } - - if len(migrationReq.SecretScope) == 0 { - promptConfirm = true - migrationReq.SecretScope = SelectInput("Scope for secrets & secret managers:", scopes, Project) - } - - if len(migrationReq.ConnectorScope) == 0 { - promptConfirm = true - migrationReq.ConnectorScope = SelectInput("Scope for connectors:", scopes, Project) - } - - if len(migrationReq.TemplateScope) == 0 { - promptConfirm = true - migrationReq.TemplateScope = SelectInput("Scope for templates:", scopes, Project) - } - - return promptConfirm -} - -func PromptOrgAndProject() bool { - promptConfirm := false - promptOrg := len(migrationReq.OrgIdentifier) == 0 - promptProject := len(migrationReq.ProjectIdentifier) == 0 - - if promptOrg { - promptConfirm = true - migrationReq.OrgIdentifier = TextInput("Which Org?") - } - if promptProject { - promptConfirm = true - migrationReq.ProjectIdentifier = TextInput("Which Project?") - } - return promptConfirm -} - func logMigrationDetails() { log.WithFields(log.Fields{ "Account": migrationReq.Account, "SecretScope": migrationReq.SecretScope, "ConnectorScope": migrationReq.ConnectorScope, + "TemplateScope": migrationReq.TemplateScope, "AppID": migrationReq.AppId, "OrgIdentifier": migrationReq.OrgIdentifier, "ProjectIdentifier": migrationReq.ProjectIdentifier, }).Info("Migration details") } -func migrateAccountLevelEntities(*cli.Context) error { - promptConfirm := PromptDefaultInputs() - - promptOrg := false - promptProject := false - // Based on the scopes of entities determine the destination details - if migrationReq.SecretScope == Project || migrationReq.ConnectorScope == Project { - promptOrg = len(migrationReq.OrgIdentifier) == 0 - promptProject = len(migrationReq.ProjectIdentifier) == 0 - } else if migrationReq.SecretScope == Org || migrationReq.ConnectorScope == Org { - promptOrg = len(migrationReq.OrgIdentifier) == 0 - } - - if promptOrg { - promptConfirm = true - migrationReq.OrgIdentifier = TextInput("Which Org?") - } - if promptProject { - promptConfirm = true - migrationReq.ProjectIdentifier = TextInput("Which Project?") - } - - logMigrationDetails() - - // We confirm if they wish to proceed or not - if promptConfirm { - confirm := ConfirmInput("Do you wish to proceed importing all secret managers, secrets & connectors?") - if !confirm { - os.Exit(1) - } - } - - // Finally Make the API calls to create all entities - url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) - - // Create Secret Managers - log.Info("Importing all secret managers from CG to NG...") - CreateEntity(url, migrationReq.Auth, getReqBody(SecretManager, Filter{ - Type: All, - })) - log.Info("Imported all secret managers.") - - // Create Secrets - log.Info("Importing all secrets from CG to NG...") - CreateEntity(url, migrationReq.Auth, getReqBody(Secret, Filter{ - Type: All, - })) - log.Info("Imported all secrets.") - - // Create Connectors - log.Info("Importing all connectors from CG to NG....") - CreateEntity(url, migrationReq.Auth, getReqBody(Connector, Filter{ - Type: All, - })) - log.Info("Imported all connectors.") - - return nil -} - func cliWrapper(fn cliFnWrapper, ctx *cli.Context) error { if migrationReq.Debug { log.SetLevel(log.DebugLevel) @@ -183,74 +68,6 @@ func cliWrapper(fn cliFnWrapper, ctx *cli.Context) error { return fn(ctx) } -func migrateApp(*cli.Context) error { - promptConfirm := PromptDefaultInputs() - if len(migrationReq.AppId) == 0 { - promptConfirm = true - migrationReq.AppId = TextInput("Please provide the application ID of the app that you wish to import -") - } - - promptConfirm = PromptOrgAndProject() || promptConfirm - - logMigrationDetails() - - if promptConfirm { - confirm := ConfirmInput("Do you want to proceed with app migration?") - if !confirm { - log.Fatal("Aborting...") - } - } - - url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) - // Migrating the app - log.Info("Importing the application....") - CreateEntity(url, migrationReq.Auth, getReqBody(Application, Filter{ - AppId: migrationReq.AppId, - })) - log.Info("Imported the application.") - - return nil -} - -func migrateWorkflows(*cli.Context) error { - promptConfirm := PromptDefaultInputs() - if len(migrationReq.AppId) == 0 { - promptConfirm = true - migrationReq.AppId = TextInput("Please provide the application ID of the app containing the workflows -") - } - - if len(migrationReq.WorkflowIds) == 0 { - promptConfirm = true - migrationReq.WorkflowIds = TextInput("Provide the workflows that you wish to import as template as comma separated values(e.g. workflow1,workflow2)") - } - - if len(migrationReq.WorkflowScope) == 0 { - migrationReq.WorkflowScope = SelectInput("Scope for workflows:", scopes, Project) - } - - promptConfirm = PromptOrgAndProject() || promptConfirm - - logMigrationDetails() - - if promptConfirm { - confirm := ConfirmInput("Do you want to proceed with workflows migration?") - if !confirm { - log.Fatal("Aborting...") - } - } - - url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) - // Migrating the app - log.Info("Importing the workflows....") - CreateEntity(url, migrationReq.Auth, getReqBody(Workflow, Filter{ - WorkflowIds: strings.Split(migrationReq.WorkflowIds, ","), - AppId: migrationReq.AppId, - })) - log.Info("Imported the workflows.") - - return nil -} - func init() { // Log as JSON instead of the default ASCII formatter. log.SetFormatter(&log.TextFormatter{ @@ -268,94 +85,120 @@ func init() { } func main() { + globalFlags := []cli.Flag{ + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "env", + Usage: "possible values - Prod, QA, Dev", + Destination: &migrationReq.Environment, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "account", + Usage: "`ACCOUNT` that you wish to migrate", + Destination: &migrationReq.Account, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "api-key", + Usage: "`API_KEY` to authenticate & authorise the migration.", + Destination: &migrationReq.Auth, + EnvVars: []string{"HARNESS_MIGRATOR_AUTH"}, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "secret-scope", + Usage: "`SCOPE` to create secrets in. Possible values - account, org, project", + Destination: &migrationReq.SecretScope, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "connector-scope", + Usage: "`SCOPE` to create connectors in. Possible values - account, org, project", + Destination: &migrationReq.ConnectorScope, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "workflow-scope", + Usage: "`SCOPE` to create workflows in. Possible values - account, org, project", + Destination: &migrationReq.WorkflowScope, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "template-scope", + Usage: "`SCOPE` to create templates in. Possible values - account, org, project", + Destination: &migrationReq.TemplateScope, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "org", + Usage: "organisation `IDENTIFIER` in next gen", + Destination: &migrationReq.OrgIdentifier, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "project", + Usage: "project `IDENTIFIER` in next gen", + Destination: &migrationReq.ProjectIdentifier, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "app", + Usage: "`APP_ID` in current gen", + Destination: &migrationReq.AppId, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "workflows", + Usage: "workflows as comma separated values `workflowId1,workflowId2`", + Destination: &migrationReq.WorkflowIds, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "pipelines", + Usage: "pipelines as comma separated values `pipeline1,pipeline2`", + Destination: &migrationReq.WorkflowIds, + }), + altsrc.NewStringFlag(&cli.StringFlag{ + Name: "load", + Usage: "`FILE` to load flags from", + Destination: &migrationReq.File, + }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "insecure", + Usage: "allow insecure API requests. This is automatically set to true if environment is Dev", + Destination: &migrationReq.AllowInsecureReq, + }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "debug", + Usage: "print debug level logs", + Destination: &migrationReq.Debug, + }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "json", + Usage: "log as JSON instead of standard ASCII formatter", + Destination: &migrationReq.Json, + }), + } app := &cli.App{ Name: "harness-upgrade", Version: Version, Usage: "Upgrade Harness CD from Current Gen to Next Gen!", EnableBashCompletion: true, + Suggest: true, Commands: []*cli.Command{ { Name: "app", - Usage: "Import an app into a existing project by providing the `appId`", + Usage: "Import an app into an existing project by providing the `appId`", Action: func(context *cli.Context) error { return cliWrapper(migrateApp, context) }, }, { Name: "workflows", - Usage: "Import workflows as stage or pipeline templates into a existing project by providing the `appId` & `workflowIds`", + Usage: "Import workflows as stage or pipeline templates by providing the `appId` & `workflowIds`", Action: func(context *cli.Context) error { return cliWrapper(migrateWorkflows, context) }, }, - }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "env", - Usage: "possible values - Prod, QA, Dev", - Destination: &migrationReq.Environment, - }, - &cli.StringFlag{ - Name: "account", - Usage: "`id` of the account that you wish to migrate", - Destination: &migrationReq.Account, - }, - &cli.StringFlag{ - Name: "secret-scope", - Usage: "`scope` to create secrets in. Possible values - account, org, project", - Destination: &migrationReq.SecretScope, - }, - &cli.StringFlag{ - Name: "connector-scope", - Usage: "`scope` to create connectors in. Possible values - account, org, project", - Destination: &migrationReq.ConnectorScope, - }, - &cli.StringFlag{ - Name: "workflow-scope", - Usage: "`scope` to create workflows in. Possible values - account, org, project", - Destination: &migrationReq.WorkflowScope, - }, - &cli.StringFlag{ - Name: "template-scope", - Usage: "`scope` to create templates in. Possible values - account, org, project", - Destination: &migrationReq.TemplateScope, - }, - &cli.StringFlag{ - Name: "org", - Usage: "organisation `identifier` in next gen", - Destination: &migrationReq.OrgIdentifier, - }, - &cli.StringFlag{ - Name: "project", - Usage: "project `identifier` in next gen", - Destination: &migrationReq.ProjectIdentifier, - }, - &cli.StringFlag{ - Name: "app", - Usage: "application `id` in current gen", - Destination: &migrationReq.AppId, - }, - &cli.StringFlag{ - Name: "workflows", - Usage: "workflows as comma separated values `workflowId1,workflowId2`", - Destination: &migrationReq.WorkflowIds, - }, - &cli.BoolFlag{ - Name: "insecure", - Usage: "allow insecure API requests. This is automatically set to true if environment is Dev", - Destination: &migrationReq.AllowInsecureReq, - }, - &cli.BoolFlag{ - Name: "debug", - Usage: "print debug level logs", - Destination: &migrationReq.Debug, - }, - &cli.BoolFlag{ - Name: "json", - Usage: "log as JSON instead of standard ASCII formatter", - Destination: &migrationReq.Json, + { + Name: "pipelines", + Usage: "Import pipelines into an existing project by providing the `appId` & `pipelineIds`", + Action: func(context *cli.Context) error { + return cliWrapper(migratePipelines, context) + }, }, }, + Before: altsrc.InitInputSourceWithContext(globalFlags, altsrc.NewYamlSourceFromFlagFunc("load")), + Flags: globalFlags, Action: func(context *cli.Context) error { return cliWrapper(migrateAccountLevelEntities, context) }, diff --git a/pipelines.go b/pipelines.go new file mode 100644 index 0000000..038ffe4 --- /dev/null +++ b/pipelines.go @@ -0,0 +1,47 @@ +package main + +import ( + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "strings" +) + +func migratePipelines(*cli.Context) error { + promptConfirm := PromptDefaultInputs() + if len(migrationReq.AppId) == 0 { + promptConfirm = true + migrationReq.AppId = TextInput("Please provide the application ID of the app containing the pipeline -") + } + + if len(migrationReq.WorkflowScope) == 0 { + promptConfirm = true + migrationReq.WorkflowScope = SelectInput("Scope for workflow to be migrated as templates:", scopes, Project) + } + + if len(migrationReq.PipelineIds) == 0 { + promptConfirm = true + migrationReq.PipelineIds = TextInput("Provide the pipelines that you wish to import as template as comma separated values(e.g. pipeline1,pipeline2)") + } + + promptConfirm = PromptOrgAndProject([]string{migrationReq.WorkflowScope, migrationReq.SecretScope, migrationReq.ConnectorScope, migrationReq.TemplateScope}) || promptConfirm + + logMigrationDetails() + + if promptConfirm { + confirm := ConfirmInput("Do you want to proceed with pipeline migration?") + if !confirm { + log.Fatal("Aborting...") + } + } + + url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) + // Migrating the pipelines + log.Info("Importing the pipelines....") + CreateEntity(url, migrationReq.Auth, getReqBody(Pipeline, Filter{ + PipelineIds: strings.Split(migrationReq.PipelineIds, ","), + AppId: migrationReq.AppId, + })) + log.Info("Imported the pipelines.") + + return nil +} diff --git a/prompts.go b/prompts.go new file mode 100644 index 0000000..5c63ec3 --- /dev/null +++ b/prompts.go @@ -0,0 +1,62 @@ +package main + +import ( + "crypto/tls" + "net/http" +) + +func PromptDefaultInputs() bool { + promptConfirm := false + + if len(migrationReq.Environment) == 0 { + promptConfirm = true + migrationReq.Environment = SelectInput("Which environment?", []string{Dev, QA, Prod, Prod3}, Dev) + } + + // Check if auth is provided. If not provided then request for one + if len(migrationReq.Auth) == 0 { + migrationReq.Auth = TextInput("The environment variable 'HARNESS_MIGRATOR_AUTH' is not set. What is the api key?") + } + + if migrationReq.Environment == "Dev" || migrationReq.AllowInsecureReq { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + if len(migrationReq.Account) == 0 { + promptConfirm = true + migrationReq.Account = TextInput("Account that you wish to migrate:") + } + + if len(migrationReq.SecretScope) == 0 { + promptConfirm = true + migrationReq.SecretScope = SelectInput("Scope for secrets & secret managers:", scopes, Project) + } + + if len(migrationReq.ConnectorScope) == 0 { + promptConfirm = true + migrationReq.ConnectorScope = SelectInput("Scope for connectors:", scopes, Project) + } + + if len(migrationReq.TemplateScope) == 0 { + promptConfirm = true + migrationReq.TemplateScope = SelectInput("Scope for templates:", scopes, Project) + } + + return promptConfirm +} + +func PromptOrgAndProject(scope []string) bool { + promptConfirm := false + promptOrg := len(migrationReq.OrgIdentifier) == 0 && ContainsAny(scope, []string{Org, Project}) + promptProject := len(migrationReq.ProjectIdentifier) == 0 && ContainsAny(scope, []string{Project}) + + if promptOrg { + promptConfirm = true + migrationReq.OrgIdentifier = TextInput("Which Org?") + } + if promptProject { + promptConfirm = true + migrationReq.ProjectIdentifier = TextInput("Which Project?") + } + return promptConfirm +} diff --git a/templates/account.yaml b/templates/account.yaml new file mode 100644 index 0000000..2b4e7dd --- /dev/null +++ b/templates/account.yaml @@ -0,0 +1,8 @@ +env: Dev +api-key: +account: kmpySmUISimoRrJL6NL73w +project: demo +org: default +secret-scope: project +connector-scope: project +template-scope: project diff --git a/templates/app.yaml b/templates/app.yaml new file mode 100644 index 0000000..22e98a4 --- /dev/null +++ b/templates/app.yaml @@ -0,0 +1,10 @@ +env: Dev +api-key: +account: kmpySmUISimoRrJL6NL73w +app: qGIv5p1gQqCrdbrsQT2Tig +project: demo +org: default +secret-scope: project +connector-scope: project +template-scope: project +workflow-scope: project diff --git a/templates/pipelines.yaml b/templates/pipelines.yaml new file mode 100644 index 0000000..e2cb0c9 --- /dev/null +++ b/templates/pipelines.yaml @@ -0,0 +1,11 @@ +env: Dev +api-key: +account: kmpySmUISimoRrJL6NL73w +app: qGIv5p1gQqCrdbrsQT2Tig +pipelines: "iQmdMl50ROK2XMAQujqmeA,1rOHou5yRJ613iwBXsDjnA,XUDPQH6mRGquxa7-Sutuwg,lSkeH8O4R32KUSJmml9pDA,ILA50kgJQKa3j3s35-h55Q" +project: demo +org: default +secret-scope: project +connector-scope: project +template-scope: project +workflow-scope: project diff --git a/templates/workflows.yaml b/templates/workflows.yaml new file mode 100644 index 0000000..ac6657c --- /dev/null +++ b/templates/workflows.yaml @@ -0,0 +1,11 @@ +env: Dev +api-key: +account: kmpySmUISimoRrJL6NL73w +app: qGIv5p1gQqCrdbrsQT2Tig +workflows: "iQmdMl50ROK2XMAQujqmeA,1rOHou5yRJ613iwBXsDjnA,XUDPQH6mRGquxa7-Sutuwg,lSkeH8O4R32KUSJmml9pDA,ILA50kgJQKa3j3s35-h55Q" +project: demo +org: default +secret-scope: project +connector-scope: project +template-scope: project +workflow-scope: project diff --git a/types.go b/types.go index ea49014..3c4227a 100644 --- a/types.go +++ b/types.go @@ -7,6 +7,7 @@ type Filter struct { Type ImportType `json:"importType"` AppId string `json:"appId"` WorkflowIds []string `json:"workflowIds"` + PipelineIds []string `json:"pipelineIds"` } type DestinationDetails struct { diff --git a/workflows.go b/workflows.go new file mode 100644 index 0000000..dd18c41 --- /dev/null +++ b/workflows.go @@ -0,0 +1,47 @@ +package main + +import ( + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "strings" +) + +func migrateWorkflows(*cli.Context) error { + promptConfirm := PromptDefaultInputs() + if len(migrationReq.AppId) == 0 { + promptConfirm = true + migrationReq.AppId = TextInput("Please provide the application ID of the app containing the workflows -") + } + + if len(migrationReq.WorkflowIds) == 0 { + promptConfirm = true + migrationReq.WorkflowIds = TextInput("Provide the workflows that you wish to import as template as comma separated values(e.g. workflow1,workflow2)") + } + + if len(migrationReq.WorkflowScope) == 0 { + promptConfirm = true + migrationReq.WorkflowScope = SelectInput("Scope for workflows:", scopes, Project) + } + + promptConfirm = PromptOrgAndProject([]string{migrationReq.WorkflowScope, migrationReq.SecretScope, migrationReq.ConnectorScope, migrationReq.TemplateScope}) || promptConfirm + + logMigrationDetails() + + if promptConfirm { + confirm := ConfirmInput("Do you want to proceed with workflows migration?") + if !confirm { + log.Fatal("Aborting...") + } + } + + url := GetUrl(migrationReq.Environment, "save/v2", migrationReq.Account) + // Migrating the app + log.Info("Importing the workflows....") + CreateEntity(url, migrationReq.Auth, getReqBody(Workflow, Filter{ + WorkflowIds: strings.Split(migrationReq.WorkflowIds, ","), + AppId: migrationReq.AppId, + })) + log.Info("Imported the workflows.") + + return nil +}