github.com/GoogleCloudPlatform/deploystack@v1.12.8/tui/tui.go (about) 1 // Copyright 2023 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package tui provides a BubbleTea powered tui for Deploystack. All rendering 16 // should happen within this package. 17 package tui 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 24 "cloud.google.com/go/domains/apiv1beta1/domainspb" 25 "github.com/GoogleCloudPlatform/deploystack/config" 26 "github.com/GoogleCloudPlatform/deploystack/gcloud" 27 "github.com/charmbracelet/bubbles/spinner" 28 tea "github.com/charmbracelet/bubbletea" 29 "google.golang.org/api/cloudbilling/v1" 30 "google.golang.org/api/cloudresourcemanager/v1" 31 "google.golang.org/api/compute/v1" 32 ) 33 34 const ( 35 explainText = "DeployStack will walk you through setting some options for the stack this solutions installs. Most questions have a default that you can choose by hitting the Enter key." 36 appTitle = "DeployStack" 37 contactfile = "contact.yaml.tmp" 38 validationPhoneNumber = "phonenumber" 39 validationYesOrNo = "yesorno" 40 validationInteger = "integer" 41 ) 42 43 var ( 44 spinnerType = spinner.Line 45 ) 46 47 // ErrorCustomNotValidPhoneNumber is the error you get when you fail phone 48 // number validation. 49 var ErrorCustomNotValidPhoneNumber = fmt.Errorf("not a valid phone number") 50 51 type errMsg struct { 52 err error 53 quit bool 54 usermsg string 55 target string 56 } 57 58 func (e errMsg) Error() string { return e.err.Error() } 59 60 type successMsg struct { 61 msg string 62 unset bool 63 } 64 65 // UIClient interface encapsulates all of the calls to gcloud that one needs to 66 // make the TUI work 67 type UIClient interface { 68 // CloudResourceManager 69 ProjectIDGet() (string, error) 70 ProjectList() ([]gcloud.ProjectWithBilling, error) 71 ProjectParentGet(project string) (*cloudresourcemanager.ResourceId, error) 72 ProjectCreate(project, parent, parentType string) error 73 ProjectNumberGet(id string) (string, error) 74 ProjectIDSet(id string) error 75 // Compute Engine 76 RegionList(project, product string) ([]string, error) 77 ZoneList(project, region string) ([]string, error) 78 ImageLatestGet(project, imageproject, imagefamily string) (string, error) 79 MachineTypeList(project, zone string) (*compute.MachineTypeList, error) 80 MachineTypeFamilyList(imgs *compute.MachineTypeList) gcloud.LabeledValues 81 MachineTypeListByFamily(imgs *compute.MachineTypeList, family string) gcloud.LabeledValues 82 ImageList(project, imageproject string) (*compute.ImageList, error) 83 ImageTypeListByFamily(imgs *compute.ImageList, project, family string) gcloud.LabeledValues 84 ImageFamilyList(imgs *compute.ImageList) gcloud.LabeledValues 85 // Billing 86 BillingAccountList() ([]*cloudbilling.BillingAccount, error) 87 BillingAccountAttach(project, account string) error 88 // Domains 89 DomainIsAvailable(project, domain string) (*domainspb.RegisterParameters, error) 90 DomainIsVerified(project, domain string) (bool, error) 91 DomainRegister(project string, domaininfo *domainspb.RegisterParameters, contact gcloud.ContactData) error 92 // ServiceUsage 93 ServiceEnable(project string, service gcloud.Service) error 94 ServiceIsEnabled(project string, service gcloud.Service) (bool, error) 95 } 96 97 // Run takes a deploystack configuration and walks someone through all of the 98 // input needed to run the eventual terraform 99 func Run(s *config.Stack, useMock bool) { 100 if len(os.Getenv("DEBUG")) > 0 { 101 f, err := tea.LogToFile("debug.log", "debug") 102 if err != nil { 103 fmt.Println("fatal:", err) 104 os.Exit(1) 105 } 106 defer f.Close() 107 } 108 109 defaultUserAgent := fmt.Sprintf("deploystack/%s", s.Config.Name) 110 111 client := gcloud.NewClient(context.Background(), defaultUserAgent) 112 q := NewQueue(s, &client) 113 114 if useMock { 115 q = NewQueue(s, GetMock(1)) 116 } 117 118 q.InitializeUI() 119 120 p := tea.NewProgram(q.Start(), tea.WithAltScreen()) 121 if _, err := p.Run(); err != nil { 122 Fatal(err) 123 } 124 125 if q.Get("halted") != nil { 126 Fatal(nil) 127 } 128 129 s.TerraformFile("terraform.tfvars") 130 131 fmt.Print("\n\n") 132 fmt.Print(titleStyle.Render("Deploystack")) 133 fmt.Print("\n") 134 fmt.Print(subTitleStyle.Render(s.Config.Title)) 135 fmt.Print("\n") 136 fmt.Print(strong.Render("Installation will proceed with these settings")) 137 fmt.Print(q.getSettings()) 138 } 139 140 // PreCheck handles presenting a choice to a user amongst multiple stacks 141 func PreCheck(reports []config.Report) string { 142 143 q := NewQueue(nil, GetMock(0)) 144 q.Save("reports", reports) 145 146 appHeader := newHeader(appTitle, "Multiple Stacks Detected") 147 firstPage := newPicker("Please pick a stack to use", "Finding stacks", "stack", "", handleReports(&q)) 148 firstPage.showProgress = false 149 firstPage.omitFromSettings = true 150 firstPage.addPostProcessor(handleStackSelection) 151 152 q.header = appHeader 153 q.add(&firstPage) 154 155 p := tea.NewProgram(q.Start(), tea.WithAltScreen()) 156 if _, err := p.Run(); err != nil { 157 Fatal(err) 158 } 159 response := q.Get("stack").(string) 160 161 fmt.Print("\n\n") 162 fmt.Print(titleStyle.Render("Deploystack")) 163 fmt.Print("\n") 164 fmt.Print(subTitleStyle.Render("Stack has been chosen")) 165 fmt.Print("\n") 166 fmt.Print(strong.Render("Installation will proceed with this stack:")) 167 fmt.Print("\n") 168 fmt.Print(response) 169 fmt.Print("\n") 170 171 return response 172 173 } 174 175 // Fatal stops processing of Deploystack and halts the calling process. All 176 // with an eye towards not processing in the shell script of things go wrong. 177 func Fatal(err error) { 178 if err != nil { 179 content := `There was an issue collecting the information it takes to run this application. 180 You can try again by typing 'deploystack install' at the command prompt 181 If the issue persists, please report at: 182 https://github.com/GoogleCloudPlatform/deploystack/issues 183 ` 184 185 errmsg := errMsg{ 186 err: err, 187 usermsg: content, 188 quit: true, 189 } 190 191 msg := errorAlert{errmsg} 192 fmt.Print("\n\n") 193 fmt.Println(titleStyle.Render("DeployStack")) 194 fmt.Println(msg.Render()) 195 } 196 fmt.Printf(clear) 197 os.Exit(1) 198 }