diff --git a/Makefile b/Makefile index 1748932..a32f194 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,9 @@ export GOPROXY=https://proxy.golang.org,direct export ENV=development -LDFLAGS+=-X k8s.io/client-go/pkg/version.gitVersion=v1.0.2 +LDFLAGS+=-X k8s.io/client-go/pkg/version.gitVersion=v1.2.2 LDFLAGS+=-X k8s.io/client-go/pkg/version.gitCommit=4f75300 -LDFLAGS+=-X github.com/openshift/oc/pkg/version.versionFromGit=v1.0.2 +LDFLAGS+=-X github.com/openshift/oc/pkg/version.versionFromGit=v1.2.2 LDFLAGS+=-X github.com/openshift/oc/pkg/version.commitFromGit=4f75300 build: go build -v -o $(ROOT)/bin/arvan -ldflags="$(LDFLAGS)" $(ROOT)/cmd/arvan/*.go diff --git a/go.mod b/go.mod index 5ab9b98..ab668ab 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/spf13/cobra v1.1.1 github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect + golang.org/x/text v0.3.4 gopkg.in/yaml.v2 v2.3.0 k8s.io/api v0.20.0-beta.2 k8s.io/apimachinery v0.20.0-beta.2 diff --git a/pkg/config/config.go b/pkg/config/config.go index 642759c..8c1d6cb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,13 +5,20 @@ import ( "io/ioutil" "sync" + "github.com/arvancloud/cli/pkg/utl" + "gopkg.in/yaml.v2" ) +const ( + regionsEndpoint = "/paas/v1/regions/" +) + type configFile struct { ApiVersion string `yaml:"apiVersion"` Server string `yaml:"server"` ApiKey string `yaml:"apikey"` + Region string `yaml:"region,omitempty"` } var instance *ConfigInfo @@ -40,8 +47,16 @@ func LoadConfigFile() (bool, error) { if err != nil { return false, err } + arvanConfig.apiKey = configFileStruct.ApiKey arvanConfig.server = configFileStruct.Server + + if configFileStruct.Region != "" { + arvanConfig.server = configFileStruct.Server + regionsEndpoint + configFileStruct.Region + _, err = arvanConfig.SaveConfig() + utl.CheckErr(err) + } + return true, nil } diff --git a/pkg/config/config_info.go b/pkg/config/config_info.go index 8805766..7231c2e 100644 --- a/pkg/config/config_info.go +++ b/pkg/config/config_info.go @@ -25,24 +25,26 @@ type ConfigInfo struct { // path to arvan config directroy e.g /home/jane/.arvan homeDir string + + region string } -//GetServer returns base url to access arvan api server +// GetServer returns base url to access arvan api server func (c *ConfigInfo) GetServer() string { return c.server } -//GetApiKey returns an api key used to authorize request to arvan api server +// GetApiKey returns an api key used to authorize request to arvan api server func (c *ConfigInfo) GetApiKey() string { return c.apiKey } -//GetConfigFilePath returns path to arvan config file e.g /home/jane/.arvan/config +// GetConfigFilePath returns path to arvan config file e.g /home/jane/.arvan/config func (c *ConfigInfo) GetConfigFilePath() string { return c.configFilePath } -//GetHomeDir returns path to arvan config directroy e.g /home/jane/.arvan +// GetHomeDir returns path to arvan config directroy e.g /home/jane/.arvan func (c *ConfigInfo) GetHomeDir() string { return c.homeDir } @@ -94,6 +96,7 @@ func (c *ConfigInfo) SaveConfig() (bool, error) { ApiVersion: configFileApiVersion, Server: c.server, ApiKey: c.apiKey, + Region: c.region, } configFileStr, err := yaml.Marshal(&configFileStruct) diff --git a/pkg/paas/login.go b/pkg/paas/login.go index 21603dc..f144fbe 100644 --- a/pkg/paas/login.go +++ b/pkg/paas/login.go @@ -97,7 +97,7 @@ func NewCmdSwitchRegion(in io.Reader, out, errout io.Writer) *cobra.Command { _, err = arvanConfig.SaveConfig() utl.CheckErr(err) - err = prepareConfig(c) + err = prepareConfigSwtichRegion(c) utl.CheckErr(err) fmt.Fprintf(explainOut, "Region Switched successfully.\n") @@ -145,14 +145,14 @@ func getSelectedRegion(in io.Reader, writer io.Writer) (*config.Zone, error) { return nil, errors.New("invalid region info") } - activeZones, inactiveZones := getActiveAndInactiveZones(regions.Zones) + upZones, downZones := getUpAndDownZones(regions.Zones) - if len(activeZones) < 1 { + if len(upZones) < 1 { return nil, errors.New("no active region available") } explain := "Select arvan region:\n" - explain += sprintRegions(activeZones, inactiveZones) + explain += sprintRegions(upZones, downZones) _, err = fmt.Fprint(writer, explain) if err != nil { @@ -162,17 +162,17 @@ func getSelectedRegion(in io.Reader, writer io.Writer) (*config.Zone, error) { defaultVal := "1" - if len(activeZones) == 1 { + if len(upZones) == 1 { fmt.Fprintf(writer, inputExplain+"1\n") - return &activeZones[0], nil + return &upZones[0], nil } - validator := regionValidator{len(activeZones)} + validator := regionValidator{len(upZones)} regionIndex := utl.ReadInput(inputExplain, defaultVal, writer, in, validator.validate) intIndex, _ := strconv.Atoi(regionIndex) - return &activeZones[intIndex-1], nil + return &upZones[intIndex-1], nil } type regionValidator struct { @@ -218,7 +218,7 @@ func sprintRegions(activeZones, inactiveRegions []config.Zone) string { return result } -func getActiveAndInactiveZones(zones []config.Zone) ([]config.Zone, []config.Zone) { +func getUpAndDownZones(zones []config.Zone) ([]config.Zone, []config.Zone) { var activeZones, inactiveZones []config.Zone for i := 0; i < len(zones); i++ { if zones[i].Status == "UP" { diff --git a/pkg/paas/migration.go b/pkg/paas/migration.go index f38042d..db9643f 100644 --- a/pkg/paas/migration.go +++ b/pkg/paas/migration.go @@ -23,9 +23,17 @@ import ( "github.com/olekukonko/tablewriter" "github.com/openshift/oc/pkg/helpers/term" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/client-go/rest" ) +var ( + migrateLong = ` + Migration of user's namespaces from one region to another + ` +) + const ( migrationEndpoint = "/paas/v1/%s/migrate" redColor = "\033[31m" @@ -97,7 +105,7 @@ func NewCmdMigrate(in io.Reader, out, errout io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "migrate", Short: "Migrate namespaces to destination region", - Long: loginLong, + Long: migrateLong, Run: func(c *cobra.Command, args []string) { explainOut := term.NewResponsiveWriter(out) c.SetOutput(explainOut) @@ -180,6 +188,16 @@ func reMigrationConfirm(in io.Reader, writer io.Writer) bool { return value == "y" } +// newProjectConfirm makes sure that user enters yes/no correctly. +func newProjectConfirm(in io.Reader, writer io.Writer) bool { + inputExplain := "Do you want to continue?[y/N]: " + + defaultVal := "N" + + value := utl.ReadInput(inputExplain, defaultVal, writer, in, confirmationValidate) + return value == "y" +} + // confirmationValidate checks yes/no entry func confirmationValidate(input string) (bool, error) { if input != "y" && input != "N" { @@ -266,13 +284,13 @@ func sprintProjects(projects []string) string { // migrationConfirm gets confirmation of proceeding namespace migration by asking user to enter namespace's name. func migrationConfirm(project, region string, in io.Reader, writer io.Writer) bool { - explain := fmt.Sprintf("\nYou're about to migrate \"%s\" from region \"%s\" to \"%s\".\n", project, getCurrentRegion(), region) + explain := fmt.Sprintf("\nYou're about to migrate \"%s\" from region \"%s\" to \"%s\".\n\n"+yellowColor+"WARNING:\nThis will STOP applications during migration process. Your data would still be safe and available in source region. Migration is running in the background and may take a while. You can optionally detach(Ctrl+C) for now and continue monitoring the process after using 'arvan paas migrate'."+resetColor+"\n\n", project, getCurrentRegion(), region) _, err := fmt.Fprint(writer, explain) if err != nil { return false } - inputExplain := fmt.Sprintf(yellowColor+"\nWARNING:\nThis will STOP applications during migration process. Your data would still be safe and available in source region. Migration is running in the background and may take a while. You can optionally detach(Ctrl+C) for now and continue monitoring the process after using 'arvan paas migrate'."+resetColor+"\n\nPlease enter project's name [%s] to proceed: ", project) + inputExplain := fmt.Sprintf("Please enter project's name [%s] to proceed: ", project) defaultVal := "" @@ -362,7 +380,9 @@ func sprintResponse(response ProgressResponse, w io.Writer) error { detail = s.Data.Detail } - responseStr += fmt.Sprintf("\t%s \t\t\t%s\t%s\n", s.Title, strings.Title(s.State), detail) + caser := cases.Title(language.English) + + responseStr += fmt.Sprintf("\t%s \t\t\t%s\t%s\n", s.Title, caser.String(s.State), detail) } fmt.Fprintf(w, "%s", responseStr) @@ -456,6 +476,7 @@ func httpGet(endpoint string) (*ProgressResponse, error) { var response ProgressResponse err = json.Unmarshal(responseBody, &response) if err != nil { + failureOutput("Migration is running in the background. You can continue monitoring the process using 'arvan paas migrate'.") return nil, err } @@ -521,9 +542,7 @@ func successOutput(data StepData) { } nonFreeDomainTable.Render() - } - if len(freeSourceDomains) > 0 { gatewayTable := tablewriter.NewWriter(os.Stdout) gatewayTable.SetHeader([]string{"old gateway", "new gateway"}) @@ -543,19 +562,17 @@ func getZoneByName(name string) (*config.Zone, error) { return nil, errors.New("invalid region info") } - activeZones, _ := getActiveAndInactiveZones(regions.Zones) + upZones, _ := getUpAndDownZones(regions.Zones) - if len(activeZones) < 1 { + if len(upZones) < 1 { return nil, errors.New("no active region available") } - for i, zone := range activeZones { + for i, zone := range upZones { if zone.Name == name { - return &activeZones[i], nil + return &upZones[i], nil } } - log.Printf("destination region not found") - - return nil, nil + return nil, errors.New("destination region not found") } diff --git a/pkg/paas/paas.go b/pkg/paas/paas.go index f217702..e0a4a39 100644 --- a/pkg/paas/paas.go +++ b/pkg/paas/paas.go @@ -61,6 +61,36 @@ func NewCmdPaas() *cobra.Command { fmt.Fprint(w, strings.Repeat("*", 50)) fmt.Fprint(w, "\n") } + + // To warn user not to duplicate projects if they have a migration plan + if cmd.Name() == "new-project" { + regions, err := api.GetZones() + if err != nil { + failureOutput("failed to get zones") + utl.CheckErr(errors.New("failed to get zones")) + } + + if len(regions.Zones) < 1 { + failureOutput("invalid region info") + utl.CheckErr(errors.New("invalid region info")) + } + + currentRegionAbbr := getCurrentRegion() + + currentRegionName := currentRegionAbbr[strings.LastIndex(currentRegionAbbr, "-")+1:] + + currentRegion, err := getZoneByName(currentRegionName) + utl.CheckErr(err) + + _, inactiveZones := getActiveAndInactiveZones(regions.Zones) + if len(inactiveZones) > 0 && currentRegion.Active { + fmt.Print(yellowColor + "\nWARNING: " + resetColor + "If you have any intention to migrate projects, do not try to create a new project in destination region!\n\n") + + if !newProjectConfirm(in, out) { + utl.CheckErr(errors.New("")) + } + } + } } return paasCommand @@ -105,6 +135,33 @@ func prepareConfig(cmd *cobra.Command) error { return nil } +func prepareConfigSwtichRegion(cmd *cobra.Command) error { + // #TODO do not use InsecureSkipVerify + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + username, httpStatusCode, err := whoAmI() + if err != nil { + if httpStatusCode == 401 { + return fmt.Errorf("%v\n%s", err, `Try "arvan login".`) + } + if httpStatusCode >= 500 { + return fmt.Errorf("%v\n%s", err, `Please try again later`) + } + return err + } + + projects, err := projectList() + if err != nil { + return err + } + + kubeConfigPath := paasConfigPath() + err = syncKubeConfig(kubeConfigPath, username, projects) + if err != nil { + return err + } + return nil +} + func prepareCommand(cmd *cobra.Command) error { arvanConfig := config.GetConfigInfo() kubeConfigPath := paasConfigPath() @@ -304,3 +361,15 @@ func getArvanServerDomainPort() (string, error) { result := hostnameEscaped + ":" + port return result, nil } + +func getActiveAndInactiveZones(zones []config.Zone) ([]config.Zone, []config.Zone) { + var activeZones, inactiveZones []config.Zone + for i := 0; i < len(zones); i++ { + if zones[i].Active { + activeZones = append(activeZones, zones[i]) + } else { + inactiveZones = append(inactiveZones, zones[i]) + } + } + return activeZones, inactiveZones +}