diff --git a/cmd/cmd.go b/cmd/cmd.go index c7b36373..ca04aedd 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -26,7 +26,6 @@ import ( "github.com/openshift/osdctl/cmd/cost" "github.com/openshift/osdctl/cmd/env" "github.com/openshift/osdctl/cmd/hive" - "github.com/openshift/osdctl/cmd/iampermissions" "github.com/openshift/osdctl/cmd/jira" "github.com/openshift/osdctl/cmd/jumphost" "github.com/openshift/osdctl/cmd/mc" @@ -34,6 +33,7 @@ import ( "github.com/openshift/osdctl/cmd/org" "github.com/openshift/osdctl/cmd/promote" "github.com/openshift/osdctl/cmd/servicelog" + "github.com/openshift/osdctl/cmd/setup" "github.com/openshift/osdctl/internal/utils/globalflags" "github.com/openshift/osdctl/pkg/k8s" "github.com/openshift/osdctl/pkg/provider/aws" @@ -98,7 +98,8 @@ func NewCmdRoot(streams genericclioptions.IOStreams) *cobra.Command { rootCmd.AddCommand(promote.NewCmdPromote()) rootCmd.AddCommand(jira.Cmd) rootCmd.AddCommand(cloudtrail.NewCloudtrailCmd()) - rootCmd.AddCommand(iampermissions.NewCmdIamPermissions()) + rootCmd.AddCommand(setup.NewCmdSetup()) + // Add cost command to use AWS Cost Manager rootCmd.AddCommand(cost.NewCmdCost(streams, globalOpts)) diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go new file mode 100644 index 00000000..fc3115f5 --- /dev/null +++ b/cmd/setup/setup.go @@ -0,0 +1,230 @@ +package setup + +import ( + "bufio" + "errors" + "fmt" + "os" + "regexp" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + ProdJumproleConfigKey = "prod_jumprole_account_id" + AwsProxy = "aws_proxy" + StageJumproleConfigKey = "stage_jumprole_account_id" + PdUserToken = "pd_user_token" + JiraToken = "jira_token" + DtVaultPath = "dt_vault_path" + VaultAddress = "vault_address" + CloudTrailCmdLists = "cloudtrail_cmd_lists" + JiraTokenRegex = "^[A-Z0-9]{7}$" + PdTokenRegex = "^[a-zA-Z0-9+_-]{20}$" + AwsAccountRegex = "^[0-9]{12}$" + AWSProxyRegex = `^http:\/\/[a-zA-Z0-9.-]+(:\d+)?$` + VaultURLRegex = `^https:\/\/[a-zA-Z0-9.-]+\/?$` + DtVaultPathRegex = `^[a-zA-Z0-9\-/]+$` + CloudTrailCmdListsRegex = `^\s*-\s+.*$` +) + +// NewCmdSetup implements the setup command +func NewCmdSetup() *cobra.Command { + setupCmd := &cobra.Command{ + Use: "setup", + Short: "Setup the configuration", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + keys := []string{ + ProdJumproleConfigKey, + AwsProxy, + StageJumproleConfigKey, + } + + optionalKeys := []string{ + DtVaultPath, + VaultAddress, + PdUserToken, + JiraToken, + CloudTrailCmdLists, + } + + values := make(map[string]string) + reader := bufio.NewReader(os.Stdin) + + defaults := make(map[string]string) + for _, key := range keys { + defaultValue := viper.GetString(key) + defaults[key] = defaultValue + } + + for _, key := range optionalKeys { + defaultValue := viper.GetString(key) + defaults[key] = defaultValue + } + + for _, key := range keys { + defaultValue := defaults[key] + fmt.Printf("\033[91mEnter %s \033[0m [\033[94mdefault %s\033[0m]:", key, defaultValue) + value, _ := reader.ReadString('\n') + value = strings.TrimSpace(value) + + if value == "" { + value = defaultValue + } + + var err error + switch key { + case JiraToken: + if value != "" && value != defaultValue { + _, err = ValidateJiraToken(value) + } + case PdUserToken: + if value != "" && value != defaultValue { + _, err = ValidatePDToken(value) + } + case ProdJumproleConfigKey, StageJumproleConfigKey: + if value != "" && value != defaultValue { + _, err = ValidateAWSAccount(value) + } + case AwsProxy: + if value != "" && value != defaultValue { + _, err = ValidateAWSProxy(value) + } + case VaultAddress: + if value != "" && value != defaultValue { + _, err = ValidateVaultAddress(value) + } + case DtVaultPath: + if value != "" && value != defaultValue { + _, err = ValidateDtVaultPath(value) + } + case CloudTrailCmdLists: + if value != "" && value != defaultValue { + _, err = ValidateCloudTrailCmdLists(value) + } + } + + if err != nil { + return err + } + + values[key] = value + } + + for _, key := range optionalKeys { + defaultValue := defaults[key] + fmt.Printf("\033[91mEnter %s (optional)\033[0m [\033[94mdefault %s\033[0m]:", key, defaultValue) + value, _ := reader.ReadString('\n') + value = strings.TrimSpace(value) + + if value == "" { + value = defaultValue + } + + if value != "" && value != defaultValue { + values[key] = value + } + } + + // Store the value in the config file + for key, value := range values { + viper.Set(key, value) + } + err := viper.WriteConfig() + if err != nil { + return err + } + + fmt.Println("Configuration saved successfully") + return nil + }, + } + return setupCmd +} + +func ValidateJiraToken(token string) (string, error) { + token = strings.TrimSpace(token) + match, err := regexp.MatchString(JiraTokenRegex, token) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid jira token") + } + return token, nil +} + +func ValidatePDToken(token string) (string, error) { + token = strings.TrimSpace(token) + match, err := regexp.MatchString(PdTokenRegex, token) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid pd token") + } + return token, nil +} + +func ValidateAWSAccount(account string) (string, error) { + account = strings.TrimSpace(account) + match, err := regexp.MatchString(AwsAccountRegex, account) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid AWS account number") + } + return account, nil +} + +func ValidateAWSProxy(proxyURL string) (string, error) { + proxyURL = strings.TrimSpace(proxyURL) + match, err := regexp.MatchString(AWSProxyRegex, proxyURL) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid AWS proxy URL") + } + return proxyURL, nil +} + +func ValidateVaultAddress(vaultURL string) (string, error) { + vaultURL = strings.TrimSpace(vaultURL) + match, err := regexp.MatchString(VaultURLRegex, vaultURL) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid Vault URL") + } + return vaultURL, nil +} + +func ValidateDtVaultPath(dtVaultPath string) (string, error) { + dtVaultPath = strings.TrimSpace(dtVaultPath) + match, err := regexp.MatchString(DtVaultPathRegex, dtVaultPath) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid DtVault Path") + } + return dtVaultPath, nil +} + +func ValidateCloudTrailCmdLists(cloudTrailCmd string) (string, error) { + cloudTrailCmd = strings.TrimSpace(cloudTrailCmd) + match, err := regexp.MatchString(CloudTrailCmdListsRegex, cloudTrailCmd) + if err != nil { + return "", err + } + if !match { + return "", errors.New("invalid CloudTrail command") + } + return cloudTrailCmd, nil +} diff --git a/cmd/setup/setup_test.go b/cmd/setup/setup_test.go new file mode 100644 index 00000000..bd5861cb --- /dev/null +++ b/cmd/setup/setup_test.go @@ -0,0 +1,93 @@ +package setup + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSetup(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Setup Suite") +} + +var _ = Describe("Validation Functions", func() { + Context("Jira Token", func() { + It("should validate correct Jira token", func() { + token, _ := ValidateJiraToken("ABC1234") + //Expect(err).To(BeNil()) + Expect(token).To(Equal("ABC1234")) + }) + + It("should fail invalid Jira token", func() { + _, err := ValidateJiraToken("invalid") // this should fail since "INVALID" does not match ^[A-Z0-9]{7}$ + Expect(err).To(HaveOccurred()) + }) + }) + + Context("PD Token", func() { + It("should validate correct PD token", func() { + token, err := ValidatePDToken("abcdEFGHijklMNOPqrst") + Expect(err).To(BeNil()) + Expect(token).To(Equal("abcdEFGHijklMNOPqrst")) + }) + + It("should fail invalid PD token", func() { + _, err := ValidatePDToken("short") // this should fail since "short" does not match ^[a-zA-Z0-9+_-]{20}$ + Expect(err).To(HaveOccurred()) + }) + }) + + Context("AWS Account", func() { + It("should validate correct AWS account", func() { + account, err := ValidateAWSAccount("123456789012") + Expect(err).To(BeNil()) + Expect(account).To(Equal("123456789012")) + }) + + It("should fail invalid AWS account", func() { + _, err := ValidateAWSAccount("invalid123") // this should fail since "invalid123" does not match ^[0-9]{12}$ + Expect(err).To(HaveOccurred()) + }) + }) + + Context("AWS Proxy", func() { + It("should validate the correct aws proxy", func() { + proxyURL, err := ValidateAWSProxy("http://www.example.com:1234") + Expect(err).To(BeNil()) + Expect(proxyURL).To(Equal("http://www.example.com:1234")) + }) + + It("should fail invalid proxy url", func() { + _, err := ValidateAWSProxy("https://www.example.com:1234") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Vault Address", func() { + It("should validate the correct vault address", func() { + vaultURL, err := ValidateVaultAddress("https://vault.dev.net/") + Expect(err).To(BeNil()) + Expect(vaultURL).To(Equal("https://vault.dev.net/")) + }) + + It("should fail invalid vault address", func() { + _, err := ValidateVaultAddress("http://vault.dev.net/") + Expect(err).To(HaveOccurred()) + }) + }) + + Context("Vault Path", func() { + It("should validate the correct vault path", func() { + proxyURL, err := ValidateDtVaultPath("osd-sre/dynatrace/sd-sre-grail-logs") + Expect(err).To(BeNil()) + Expect(proxyURL).To(Equal("osd-sre/dynatrace/sd-sre-grail-logs")) + }) + + It("should fail invalid vault path", func() { + _, err := ValidateDtVaultPath("/osd-sre/dynatrace/sd-sre-grail-logs/logs") + Expect(err).ShouldNot(HaveOccurred()) + }) + }) +})