diff --git a/cmd/project.go b/cmd/project.go index 1b640282..5db7c106 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -3,9 +3,14 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" "strconv" + "github.com/charmbracelet/huh" + "github.com/uselagoon/lagoon-cli/internal/util" + "github.com/uselagoon/lagoon-cli/internal/wizard/project" + "strings" "github.com/uselagoon/machinery/api/lagoon" @@ -143,10 +148,24 @@ var addProjectCmd = &cobra.Command{ if err != nil { return err } - - if err := requiredInputCheck("Project name", cmdProjectName, "git-url", gitUrl, "Production environment", productionEnvironment, "Deploytarget", strconv.Itoa(int(deploytarget))); err != nil { + interactive, err := cmd.Flags().GetBool("interactive") + if err != nil { return err } + genCmdEnabled, err := cmd.Flags().GetBool("generate-command") + if err != nil { + return err + } + if interactive && !experimentalEnabled { + return fmt.Errorf("--interactive requires experimental flag to be set in .lagoon.yml") + } + generatedCommand := "" + + if !interactive { + if err := requiredInputCheck("Project name", cmdProjectName, "git-url", gitUrl, "Production environment", productionEnvironment, "Deploytarget", strconv.Itoa(int(deploytarget))); err != nil { + return err + } + } current := lagoonCLIConfig.Current token := lagoonCLIConfig.Lagoons[current].Token @@ -157,19 +176,37 @@ var addProjectCmd = &cobra.Command{ &token, debug) - projectInput := schema.AddProjectInput{ - Name: cmdProjectName, - GitURL: gitUrl, - ProductionEnvironment: productionEnvironment, - StandbyProductionEnvironment: standbyProductionEnvironment, - Branches: branches, - PullRequests: pullrequests, - OpenshiftProjectPattern: deploytargetProjectPattern, - Openshift: deploytarget, - Subfolder: subfolder, - PrivateKey: privateKey, - BuildImage: buildImage, - RouterPattern: routerPattern, + projectInput := schema.AddProjectInput{} + if interactive { + config, err := project.RunCreateWizard(lc) + if err != nil { + if errors.Is(err, huh.ErrUserAborted) { + return nil + } + return err + } + projectInput = config.Input + if config.OrganizationDetails.Name != "" { + organizationName = config.OrganizationDetails.Name + } + generatedCommand = util.GenerateCLICommand(config) + } + + if !interactive { + projectInput = schema.AddProjectInput{ + Name: cmdProjectName, + GitURL: gitUrl, + ProductionEnvironment: productionEnvironment, + StandbyProductionEnvironment: standbyProductionEnvironment, + Branches: branches, + PullRequests: pullrequests, + OpenshiftProjectPattern: deploytargetProjectPattern, + Openshift: deploytarget, + Subfolder: subfolder, + PrivateKey: privateKey, + BuildImage: buildImage, + RouterPattern: routerPattern, + } } if orgOwnerProvided { projectInput.AddOrgOwner = &orgOwner @@ -210,12 +247,19 @@ var addProjectCmd = &cobra.Command{ Result: "success", ResultData: map[string]interface{}{ "Project Name": project.Name, - "GitURL": gitUrl, + "GitURL": projectInput.GitURL, }, } if organizationName != "" { resultData.ResultData["Organization"] = organizationName } + if interactive && genCmdEnabled { + cmdPrefix := "lagoon add project" + if cmdLagoon != "" { + cmdPrefix = fmt.Sprintf("lagoon --lagoon %s add project", cmdLagoon) + } + resultData.ResultData["Generated Command"] = cmdPrefix + generatedCommand + } r := output.RenderResult(resultData, outputOptions) fmt.Fprintf(cmd.OutOrStdout(), "%s", r) return nil @@ -765,6 +809,8 @@ func init() { addProjectCmd.Flags().Bool("owner", false, "Add the user as an owner of the project") addProjectCmd.Flags().StringP("organization-name", "O", "", "Name of the Organization to add the project to") addProjectCmd.Flags().UintP("organization-id", "", 0, "ID of the Organization to add the project to") + addProjectCmd.Flags().Bool("interactive", false, "Set Interactive mode for the project creation wizard. Requires 'experimental' flag to be set in .lagoon.yml") + addProjectCmd.Flags().Bool("generate-command", false, "Displays the generated command from the project creation wizard. Requires Interactive mode to be enabled") listCmd.AddCommand(listProjectByMetadata) listProjectByMetadata.Flags().StringP("key", "K", "", "The key name of the metadata value you are querying on") diff --git a/cmd/root.go b/cmd/root.go index 76f30b3a..5d770e0a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,6 +39,7 @@ var userPath string var configFilePath string var updateDocURL = "https://uselagoon.github.io/lagoon-cli" var verboseOutput bool +var experimentalEnabled bool var skipUpdateCheck bool @@ -262,6 +263,7 @@ func initConfig() { if cmdProject.Environment != "" && cmdProjectEnvironment == "" { cmdProjectEnvironment = cmdProject.Environment } + experimentalEnabled = lagoonCLIConfig.IsFlagSet("experimental") } func yesNo(message string) bool { diff --git a/docs/commands/lagoon_add_project.md b/docs/commands/lagoon_add_project.md index 768678d8..77171bec 100644 --- a/docs/commands/lagoon_add_project.md +++ b/docs/commands/lagoon_add_project.md @@ -22,6 +22,7 @@ lagoon add project [flags] -L, --development-environments-limit uint How many environments can be deployed at one time -g, --git-url string GitURL of the project -h, --help help for project + --interactive Set Interactive mode for the project creation wizard. -j, --json string JSON string to patch --organization-id uint ID of the Organization to add the project to -O, --organization-name string Name of the Organization to add the project to diff --git a/go.mod b/go.mod index fc34a9cf..92632297 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,71 @@ module github.com/uselagoon/lagoon-cli -go 1.24.0 +go 1.24.2 -toolchain go1.24.1 +toolchain go1.24.4 require ( github.com/Masterminds/semver/v3 v3.4.0 + github.com/charmbracelet/huh v0.7.0 + github.com/charmbracelet/lipgloss v1.1.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github/v66 v66.0.0 github.com/guregu/null v4.0.0+incompatible github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc - github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/jedib0t/go-pretty/v6 v6.7.1 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/manifoldco/promptui v0.9.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/skeema/knownhosts v1.3.1 + github.com/skeema/knownhosts v1.3.2 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/uselagoon/machinery v0.0.34 - golang.org/x/crypto v0.42.0 - golang.org/x/term v0.35.0 + golang.org/x/crypto v0.43.0 + golang.org/x/term v0.36.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.5 // indirect + github.com/charmbracelet/colorprofile v0.3.1 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20251109135125-8916d276318f // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 1fc28d4e..b9d734b0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,43 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= +github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= +github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20251109135125-8916d276318f h1:OevNiFl/Y/IldUl5jtcPwEyiI3uzUsem8u7P4mzSmGI= +github.com/charmbracelet/x/exp/strings v0.0.0-20251109135125-8916d276318f/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -9,12 +47,22 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -34,8 +82,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc h1:4IZpk3M4m6ypx0IlRoEyEyY1gAdicWLMQ0NcG/gBnnA= github.com/integralist/go-findroot v0.0.0-20160518114804-ac90681525dc/go.mod h1:UlaC6ndby46IJz9m/03cZPKKkR9ykeIVBBDE3UDBdJk= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.7.1 h1:bHDSsj93NuJ563hHuM7ohk/wpX7BmRFNIsVv1ssI2/M= +github.com/jedib0t/go-pretty/v6 v6.7.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -46,21 +94,34 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 h1:guHWmqIKr4G+gQ4uYU5vcZjsUhhklRA2uOcGVfcfqis= github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -68,8 +129,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -79,17 +140,25 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/uselagoon/machinery v0.0.34 h1:5DsvXEyMeXmzQhjt11YH7+kZJueabovrwKTv0x7jQV8= github.com/uselagoon/machinery v0.0.34/go.mod h1:G0ujppuNR0BrtAnlmH8xDb9TDfayb4A36aeo0DYg7fQ= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/util/shared.go b/internal/util/shared.go new file mode 100644 index 00000000..8cf4ce53 --- /dev/null +++ b/internal/util/shared.go @@ -0,0 +1,196 @@ +package util + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" +) + +type CreateConfig struct { + Input schema.AddProjectInput + OrganizationDetails schema.Organization +} + +var fieldCmdMap = map[string]string{ + "Name": "--project", + "GitURL": "--git-url", + "Subfolder": "--subfolder", + "Openshift": "--deploytarget", + "OpenshiftProjectPattern": "--deploytarget-project-pattern", + "Branches": "--branches", + "PullRequests": "--pullrequests", + "ProductionEnvironment": "--production-environment", + "StandbyProductionEnvironment": "--standby-production-environment", + "Availability": "--availability", + "AutoIdle": "--auto-idle", + "StorageCalc": "--storage-calc", + "DevelopmentEnvironmentsLimit": "--development-environments-limit", + "PrivateKey": "--private-key", + "BuildImage": "--build-image", + "Organization": "--organization-id", + "AddOrgOwner": "--owner", + "RouterPattern": "--router-pattern", + "ProblemsUI": "--problems-ui", + "FactsUI": "--facts-ui", + "ProductionBuildPriority": "--production-build-priority", + "DevelopmentBuildPriority": "--development-build-priority", + "DeploymentsDisabled": "--deployments-disabled", +} + +type reflectFields struct { + fieldType reflect.Type + fieldValue reflect.Value +} + +func IsValidGitURL(gitUrl string) bool { + if strings.TrimSpace(gitUrl) == "" { + return false + } + + const sshPattern = `^[\w.-]+@[\w.-]+:[\w.-]+(/[\w.-]+)+(?:\.git)?$` + re := regexp.MustCompile(sshPattern) + if re.MatchString(gitUrl) { + return true + } + + parsedUrl, err := url.Parse(gitUrl) + if err != nil { + fmt.Println("Error parsing URL:", err) + return false + } + + if parsedUrl.Host == "" || parsedUrl.Scheme == "" { + return false + } + + validProtocols := map[string]bool{"git": true, "ssh": true, "http": true, "https": true, "ftp": true} + if !validProtocols[strings.ToLower(parsedUrl.Scheme)] { + return false + } + + pathParts := strings.Split(strings.Trim(parsedUrl.Path, "/"), "/") + return len(pathParts) >= 2 +} + +func QuotaCheck(quota int) string { + quotaRoute := strconv.Itoa(quota) + if quota < 0 { + quotaRoute = "∞" + } + return quotaRoute +} + +func IsValidProjectName(name string) (bool, string) { + casePattern := regexp.MustCompile("[^0-9a-z-]") + dashPattern := regexp.MustCompile("--") + + if name == "" { + return false, "Project name is required" + } + if casePattern.MatchString(name) { + return false, "Project name is invalid, only lowercase characters, numbers and dashes allowed for name" + } + if dashPattern.MatchString(name) { + return false, "Multiple consecutive dashes are not allowed for name" + } + return true, "" +} + +func GetOrgDeployTargets(lc *client.Client, orgName string) ([]schema.DeployTarget, error) { + var orgDeployTargets []schema.DeployTarget + rawOrgByName := `query organizationByNameWithDeployTargets($name: String!) { + organizationByName(name: $name) { + id + name + deployTargets{ + id + name + } + } + }` + + orgResp, err := lc.ProcessRaw(context.TODO(), rawOrgByName, map[string]interface{}{ + "name": orgName, + }) + if err != nil { + return nil, err + } + fmt.Println() + + orgData, ok := orgResp.(map[string]interface{})["organizationByName"] + if !ok { + return nil, errors.New("organization not found") + } + + orgDT, exists := orgData.(map[string]interface{})["deployTargets"] + if !exists { + return nil, errors.New("no deployTargets found in organization") + } + + odt, err := json.Marshal(orgDT) + if err != nil { + return nil, err + } + + err = json.Unmarshal(odt, &orgDeployTargets) + if err != nil { + return nil, err + } + return orgDeployTargets, nil +} + +func processFields(fieldName string, fieldValue reflect.Value) string { + cmd := "" + + if fieldName == "OrganizationDetails" { + addOrgInputField := fieldValue.FieldByName("AddOrganizationInput") + nameField := addOrgInputField.FieldByName("Name") + if nameField.IsValid() { + cmd += " " + fmt.Sprintf("%s=%v", "--organization-name", nameField.Interface()) + } + } + + if flag, exists := fieldCmdMap[fieldName]; exists { + if flag == "--owner" { + cmd += " " + fmt.Sprintf("%s=%v", flag, *(fieldValue.Interface().(*bool))) + } else { + cmd += " " + fmt.Sprintf("%s %v", flag, fieldValue.Interface()) + } + } + return cmd +} + +func GenerateCLICommand(config *CreateConfig) string { + commands := "" + + configFields := []reflectFields{ + { + fieldType: reflect.TypeOf(config.Input), + fieldValue: reflect.ValueOf(config.Input), + }, + { + fieldType: reflect.TypeOf(*config), + fieldValue: reflect.ValueOf(*config), + }, + } + + for _, field := range configFields { + for i := 0; i < field.fieldType.NumField(); i++ { + fieldName := field.fieldType.Field(i).Name + fieldValue := field.fieldValue.Field(i) + if !fieldValue.IsZero() { + commands += processFields(fieldName, fieldValue) + } + } + } + return commands +} diff --git a/internal/wizard/project/create.go b/internal/wizard/project/create.go new file mode 100644 index 00000000..c871969c --- /dev/null +++ b/internal/wizard/project/create.go @@ -0,0 +1,291 @@ +package project + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "strconv" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/uselagoon/lagoon-cli/internal/util" + "github.com/uselagoon/lagoon-cli/internal/wizard/themes" + "github.com/uselagoon/machinery/api/lagoon" + "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" +) + +func RunCreateWizard(lc *client.Client) (*util.CreateConfig, error) { + config := &util.CreateConfig{} + organizationConfirm := true + initForm := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter the project name"). + Value(&config.Input.Name). + Validate(func(name string) error { + valid, errMsg := util.IsValidProjectName(name) + if !valid { + return errors.New(errMsg) + } + if valid { + proj, err := lagoon.GetMinimalProjectByName(context.TODO(), name, lc) + if err != nil { + return err + } + if proj.Name != "" { + return fmt.Errorf("project: %s already exists", name) + } + } + return nil + }), + huh.NewConfirm(). + Title("Do you want to create this project in an Organization?"). + Value(&organizationConfirm), + ), + ).WithTheme(themes.ThemeLagoon()) + + formErr := initForm.Run() + if formErr != nil { + return nil, formErr + } + var organizations []schema.Organization + if organizationConfirm { + raw := `query allOrgsWithProjects { + allOrganizations { + id + name + description + friendlyName + quotaProject + quotaGroup + quotaNotification + quotaEnvironment + quotaRoute + projects { + id + name + } + } + }` + resp, err := lc.ProcessRaw(context.TODO(), raw, map[string]interface{}{}) + if err != nil { + return nil, err + } + + allOrgs := resp.(map[string]interface{})["allOrganizations"] + o, err := json.Marshal(allOrgs) + if err != nil { + return nil, err + } + + err = json.Unmarshal(o, &organizations) + if err != nil { + return nil, err + } + + orgValidationErr := false + form2 := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Select an organization"). + OptionsFunc(func() []huh.Option[string] { + // let's sort orgs + sort.Slice(organizations, func(i, j int) bool { + return organizations[i].Name < organizations[j].Name + }) + options := make([]huh.Option[string], len(organizations)) + for i, org := range organizations { + orgProjectCount := len(org.Projects) + if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { + quotaFullLabel := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(fmt.Sprintf("%s | Project Limit: %v/%v | ⚠️ Project Quota full, cannot assigned project to this organization.", org.Name, orgProjectCount, org.QuotaProject)) + options[i] = huh.NewOption(quotaFullLabel, org.Name) + } else { + options[i] = huh.NewOption(fmt.Sprintf("%s | Project Limit: %v/%v", org.Name, orgProjectCount, util.QuotaCheck(org.QuotaProject)), org.Name) + } + } + return options + }, &config.OrganizationDetails.Name). + Value(&config.OrganizationDetails.Name). + Validate(func(selectedOrg string) error { + for _, org := range organizations { + if org.Name == selectedOrg { + orgProjectCount := len(org.Projects) + if orgProjectCount >= org.QuotaProject && org.QuotaProject >= 0 { + orgValidationErr = true + return nil + } + } + } + return nil + }), + ), + ).WithTheme(themes.ThemeLagoon()) + + err = form2.Run() + if err != nil { + return nil, err + } + + if orgValidationErr { + var orgRetryResp string + orgRetryRespForm := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Organization quota exceeded. What would you like to do?"). + Options( + huh.NewOption("Go back to previous form", "back"), + huh.NewOption("Cancel wizard", "cancel"), + ). + Value(&orgRetryResp), + ), + ).WithTheme(themes.ThemeLagoon()) + + err := orgRetryRespForm.Run() + if err != nil { + return nil, err + } + + switch orgRetryResp { + case "back": + return RunCreateWizard(lc) + case "cancel": + return nil, huh.ErrUserAborted + } + } + } + + var deployTargets *[]schema.DeployTarget + var err error + if organizationConfirm { + orgDeploytargets, err := util.GetOrgDeployTargets(lc, config.OrganizationDetails.Name) + if err != nil { + return nil, err + } + deployTargets = &orgDeploytargets + } else { + deployTargets, err = lagoon.ListDeployTargets(context.TODO(), lc) + if err != nil { + return nil, err + } + if deployTargets == nil { + return nil, errors.New("no deployTargets found for user") + } + } + var additionalFields bool + options := make([]huh.Option[uint], len(*deployTargets)) + for i, target := range *deployTargets { + options[i] = huh.NewOption(target.Name, target.ID) + } + form3 := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Enter the Git URL for your project"). + Value(&config.Input.GitURL). + Validate(func(str string) error { + if !util.IsValidGitURL(config.Input.GitURL) { + return errors.New("invalid Git URL") + } + return nil + }), + huh.NewInput(). + Title("Enter the branch name for the production environment"). + Value(&config.Input.ProductionEnvironment), + huh.NewSelect[uint](). + Title("Select a deploy target"). + Options(options...). + Value(&config.Input.Openshift), + huh.NewConfirm(). + Title("Do you want to define any other fields?"). + Value(&additionalFields), + ), + ).WithTheme(themes.ThemeLagoon()) + + err = form3.Run() + if err != nil { + return nil, err + } + + if additionalFields { + var fields []string + additionalFieldsOptions := []huh.Option[string]{ + huh.NewOption("branches: Which branches should be deployed", "Branches"), + huh.NewOption("pullrequests: Which Pull Requests should be deployed", "PullRequests"), + } + if organizationConfirm { + additionalFieldsOptions = append(additionalFieldsOptions, huh.NewOption("owner: Add your account to the project group during creation", "AddOrgOwner")) + } + form4 := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Title("Select which fields you want to define"). + Options(additionalFieldsOptions...). + Value(&fields), + ), + ).WithTheme(themes.ThemeLagoon()) + + err = form4.Run() + if err != nil { + return nil, err + } + + if len(fields) == 0 { + return config, nil + } + + var inputs []huh.Field + var devEnvLimit string + for _, field := range fields { + switch field { + case "StandbyProductionEnvironment": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.StandbyProductionEnvironment)) + case "Branches": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.Branches)) + case "PullRequests": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.PullRequests)) + case "OpenshiftProjectPattern": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.OpenshiftProjectPattern)) + case "Subfolder": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.Subfolder)) + case "PrivateKey": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.PrivateKey)) + case "BuildImage": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.BuildImage)) + case "RouterPattern": + inputs = append(inputs, huh.NewInput().Title(fmt.Sprintf("Enter value for '%s'", field)).Value(&config.Input.RouterPattern)) + + case "DevelopmentEnvironmentsLimit": + inputs = append(inputs, huh.NewInput(). + Title(fmt.Sprintf("Enter value for '%s'", field)). + Value(&devEnvLimit). + Validate(func(s string) error { + if s == "" { + return nil + } + _, err := strconv.ParseUint(s, 10, 64) + return err + }), + ) + + case "AddOrgOwner": + config.Input.AddOrgOwner = new(bool) + inputs = append(inputs, huh.NewConfirm(). + Title("Add my user to this project"). + Value(config.Input.AddOrgOwner)) + } + } + + form5 := huh.NewForm( + huh.NewGroup(inputs...), + ).WithTheme(themes.ThemeLagoon()) + + err = form5.Run() + if err != nil { + return nil, err + } + + } + return config, nil +} diff --git a/internal/wizard/themes/ThemeLagoon.go b/internal/wizard/themes/ThemeLagoon.go new file mode 100644 index 00000000..1b18c45b --- /dev/null +++ b/internal/wizard/themes/ThemeLagoon.go @@ -0,0 +1,60 @@ +package themes + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +func ThemeLagoon() *huh.Theme { + t := huh.ThemeBase() + + var ( + normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} + lagoonBlue = lipgloss.AdaptiveColor{Light: "#4578e5", Dark: "#4578e5"} + cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"} + lagoonLightBlue = lipgloss.Color("#2094f3") + errorText = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"} + accents = lipgloss.AdaptiveColor{Light: "#364c86", Dark: "#fafafc"} + ) + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) + t.Focused.Card = t.Focused.Base + t.Focused.Title = t.Focused.Title.Foreground(lagoonBlue).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(lagoonBlue).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(lagoonBlue) + t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorText) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorText) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(accents) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(lagoonLightBlue) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(lagoonLightBlue) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(accents) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#02CF92", Dark: "#02A877"}).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ") + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(lagoonLightBlue) + t.Focused.Next = t.Focused.FocusedButton + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"}) + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(accents) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(accents) + + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Card = t.Blurred.Base + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + t.Group.Title = t.Focused.Title + t.Group.Description = t.Focused.Description + t.Help.Ellipsis = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.ShortKey = lipgloss.NewStyle().Foreground(normalFg).Bold(true) + t.Help.ShortDesc = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.ShortSeparator = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.FullKey = lipgloss.NewStyle().Foreground(normalFg).Bold(true) + t.Help.FullDesc = lipgloss.NewStyle().Foreground(lagoonBlue) + t.Help.FullSeparator = lipgloss.NewStyle().Foreground(lagoonBlue) + return t +}