tui/post_processors.go (306 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tui import ( "fmt" "strconv" "strings" "cloud.google.com/go/domains/apiv1beta1/domainspb" "github.com/GoogleCloudPlatform/deploystack/gcloud" tea "github.com/charmbracelet/bubbletea" "github.com/nyaruka/phonenumbers" ) func processProjectSelection(projectID string, q *Queue) tea.Cmd { return func() tea.Msg { if projectID != "" { if errMsg := handleProjectNumber(projectID, q); errMsg != nil { return errMsg } if err := q.client.ProjectIDSet(projectID); err != nil { return errMsg{err: err} } q.Save("currentProject", projectID) creator := q.currentKey() + projNewSuffix billing := q.currentKey() + billNewSuffix q.removeModel(creator) q.removeModel(billing) return successMsg{} } return successMsg{} } } func handleProjectNumber(projectID string, q *Queue) tea.Msg { if q.stack.Config.ProjectNumber { projectnumber, err := q.client.ProjectNumberGet(projectID) if err != nil { return errMsg{err: err} } q.stack.AddSetting("project_number", projectnumber) } return nil } func createProject(projectID string, q *Queue) tea.Cmd { return func() tea.Msg { currentProjectID := q.Get("currentProject").(string) if currentProjectID == "" { tmp, err := q.client.ProjectList() if err != nil || len(tmp) == 0 || tmp[0].ID == "" { return errMsg{err: fmt.Errorf("createProject: could not determine an alternate project for parent detection: %w ", err)} } currentProjectID = tmp[0].ID } parent, err := q.client.ProjectParentGet(currentProjectID) if err != nil { return errMsg{err: fmt.Errorf("createProject: could not determine proper parent for project: %w ", err)} } if err := q.client.ProjectCreate(projectID, parent.Id, parent.Type); err != nil { return errMsg{err: fmt.Errorf("createProject: could not create project: %w", err)} } if err := q.client.ServiceEnable(projectID, gcloud.ServiceUsage); err != nil { return errMsg{err: fmt.Errorf("createProject: could not enable service: %w", err)} } if errMsg := handleProjectNumber(projectID, q); errMsg != nil { return errMsg } if err := q.client.ProjectIDSet(projectID); err != nil { return errMsg{err: err} } qmod := q.Model("region") if qmod != nil { r := qmod.(*picker) r.querySlowText = "Getting regions can take a little extra time if this is a new project" } qmod = q.Model("zone") if qmod != nil { z := qmod.(*picker) z.querySlowText = "Getting zones can take a little extra time if this is a new project" } q.Save("currentProject", projectID) return successMsg{} } } func attachBilling(ba string, q *Queue) tea.Cmd { return func() tea.Msg { baclean := strings.ReplaceAll(ba, "billingAccounts/", "") key := strings.ReplaceAll(q.currentKey(), billNewSuffix, "") projectID := q.stack.GetSetting(key) if err := q.client.BillingAccountAttach(projectID, baclean); err != nil { return errMsg{err: fmt.Errorf("attachBilling: could not attach billing to project: %w", err)} } // If this is one of those billing for project form, let's skip // adding it to the stack settings if strings.Contains(q.currentKey(), billNewSuffix) { return successMsg{unset: true} } return successMsg{} } } func validateDomain(domain string, q *Queue) tea.Cmd { return func() tea.Msg { projectID := q.Get("currentProject").(string) domainInfo, err := q.client.DomainIsAvailable(projectID, domain) if err != nil { return errMsg{err: fmt.Errorf("validateDomain: error checking domain availability %w", err)} } q.Save("domainInfo", domainInfo) q.Save("domain", domain) if domainInfo.Availability == domainspb.RegisterParameters_UNAVAILABLE { isVerified, err := q.client.DomainIsVerified(projectID, domain) if err != nil { return errMsg{ usermsg: "Trying to validate that you own this domain failed due to an error", err: fmt.Errorf("validateDomain: error verifying domain: %s", err), } } if !isVerified { return errMsg{ usermsg: "Domain is owned by someone other than the requestor", err: fmt.Errorf("validateDomain: not owned by requestor: %w", err), } } return successMsg{} } return successMsg{} } } func registerDomain(consent string, q *Queue) tea.Cmd { return func() tea.Msg { userMsg := "There was a problem registering the domain." c := strings.ToLower(consent) if c != "y" && c != "yes" { q.stack.DeleteSetting("domain_consent") return errMsg{ usermsg: userMsg, err: fmt.Errorf("did not consent to being charged"), target: "quit", } } d := gcloud.ContactData{} contact := q.Get("contact") if contact != nil { tmp := contact.(gcloud.ContactData) if tmp.AllContacts.Email != "" { d = tmp } } if d.AllContacts.Email == "" { d = gcloud.ContactData{ AllContacts: gcloud.DomainRegistrarContact{ Email: q.stack.GetSetting("domain_email"), Phone: q.stack.GetSetting("domain_phone"), PostalAddress: gcloud.PostalAddress{ RegionCode: q.stack.GetSetting( "domain_country", ), PostalCode: q.stack.GetSetting( "domain_postalcode", ), AdministrativeArea: q.stack.GetSetting("domain_state"), Locality: q.stack.GetSetting("domain_city"), AddressLines: []string{ q.stack.GetSetting("domain_address"), }, Recipients: []string{ q.stack.GetSetting("domain_name"), }, }, }, } } q.Save("contact", d) raw := q.Get("domainInfo") domainInfo := raw.(*domainspb.RegisterParameters) projectID := q.Get("currentProject").(string) err := q.client.DomainRegister(projectID, domainInfo, d) if err != nil { q.stack.AddSetting("domain_consent", "") return errMsg{ usermsg: userMsg, err: fmt.Errorf("registerDomain: error registering domain: %w", err), target: "domain", } } domainSettings := q.stack.Settings.Search("domain_") for _, v := range domainSettings { q.stack.DeleteSetting(v.Name) } return successMsg{} } } func validateInteger(input string, q *Queue) tea.Cmd { return func() tea.Msg { _, err := strconv.Atoi(input) if err != nil { return errMsg{err: fmt.Errorf("Your answer '%s' not a valid integer", input)} } return successMsg{} } } func checkYesOrNo(input string) bool { text := strings.TrimSpace(strings.ToLower(input)) yesList := " yes y " noList := " no n " return strings.Contains(yesList+noList, text) } func validateYesOrNo(input string, q *Queue) tea.Cmd { return func() tea.Msg { text := strings.TrimSpace(strings.ToLower(input)) if !checkYesOrNo(text) { return errMsg{err: fmt.Errorf("Your answer '%s' is neither 'yes' nor 'no'", input)} } return successMsg{} } } func validatePhoneNumber(input string, q *Queue) tea.Cmd { return func() tea.Msg { _, err := massagePhoneNumber(input) if err != nil { return errMsg{err: fmt.Errorf("Your answer '%s' is not a valid phone number. Please try again", input)} } return successMsg{} } } func massagePhoneNumber(s string) (string, error) { num, err := phonenumbers.Parse(s, "US") if err != nil { return "", ErrorCustomNotValidPhoneNumber } result := phonenumbers.Format(num, phonenumbers.INTERNATIONAL) result = strings.Replace(result, " ", ".", 1) result = strings.ReplaceAll(result, "-", "") result = strings.ReplaceAll(result, " ", "") return result, nil } // TODO: see if you can test these error conditions func validateGCEDefault(input string, q *Queue) tea.Cmd { return func() tea.Msg { text := strings.TrimSpace(strings.ToLower(input)) if !checkYesOrNo(text) { return errMsg{err: fmt.Errorf("Your answer '%s' is neither 'yes' nor 'no'", input)} } if string(text[0]) == "n" { return successMsg{} } project := q.stack.GetSetting("project_id") basename := q.stack.GetSetting("basename") defaultImage, err := q.client.ImageLatestGet(project, gcloud.DefaultImageProject, gcloud.DefaultImageFamily) if err != nil { return errMsg{ err: fmt.Errorf( "validateGCEDefault: could not get DefaultImage for project(%s) family(%s): %s", gcloud.DefaultImageProject, gcloud.DefaultImageFamily, err, ), } } defaultConfig := map[string]string{ "instance-image": defaultImage, "instance-disksize": gcloud.DefaultDiskSize, "instance-disktype": gcloud.DefaultDiskType, "instance-tags": gcloud.HTTPServerTags, "instance-name": fmt.Sprintf("%s-instance", basename), "region": gcloud.DefaultRegion, "zone": gcloud.DefaultZone, "instance-machine-type": gcloud.DefaultInstanceType, } for i, v := range defaultConfig { q.stack.AddSetting(i, v) } q.removeModel("instance-webserver") q.removeModel("instance-image-project") q.removeModel("instance-machine-type-family") q.removeModel("instance-image") q.removeModel("instance-image-type") q.removeModel("instance-disksize") q.removeModel("instance-disktype") q.removeModel("instance-tags") q.removeModel("instance-name") q.removeModel("instance-machine-type") q.removeModel("region") q.removeModel("zone") q.removeModel("instance-image-family") return successMsg{} } } func validateGCEConfiguration(input string, q *Queue) tea.Cmd { return func() tea.Msg { q.stack.AddSetting("instance-tags", "") instanceWebserver := q.stack.GetSetting("instance-webserver") if instanceWebserver == "y" || input == "y" { q.stack.AddSetting("instance-tags", gcloud.HTTPServerTags) } q.stack.DeleteSetting("gce-use-defaults") q.stack.DeleteSetting("instance-webserver") q.stack.DeleteSetting("instance-image-project") q.stack.DeleteSetting("instance-machine-type-family") q.stack.DeleteSetting("instance-image-family") return successMsg{unset: true} } } func prependProject(value string, q *Queue) tea.Cmd { return func() tea.Msg { return successMsg{msg: "prependProject"} } } func handleStackSelection(stack string, q *Queue) tea.Cmd { q.Save("stack", stack) return func() tea.Msg { return successMsg{} } }