tui/tui.go (139 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 provides a BubbleTea powered tui for Deploystack. All rendering
// should happen within this package.
package tui
import (
"context"
"fmt"
"os"
"cloud.google.com/go/domains/apiv1beta1/domainspb"
"github.com/GoogleCloudPlatform/deploystack/config"
"github.com/GoogleCloudPlatform/deploystack/gcloud"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/compute/v1"
)
const (
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."
appTitle = "DeployStack"
contactfile = "contact.yaml.tmp"
validationPhoneNumber = "phonenumber"
validationYesOrNo = "yesorno"
validationInteger = "integer"
)
var (
spinnerType = spinner.Line
)
// ErrorCustomNotValidPhoneNumber is the error you get when you fail phone
// number validation.
var ErrorCustomNotValidPhoneNumber = fmt.Errorf("not a valid phone number")
type errMsg struct {
err error
quit bool
usermsg string
target string
}
func (e errMsg) Error() string { return e.err.Error() }
type successMsg struct {
msg string
unset bool
}
// UIClient interface encapsulates all of the calls to gcloud that one needs to
// make the TUI work
type UIClient interface {
// CloudResourceManager
ProjectIDGet() (string, error)
ProjectList() ([]gcloud.ProjectWithBilling, error)
ProjectParentGet(project string) (*cloudresourcemanager.ResourceId, error)
ProjectCreate(project, parent, parentType string) error
ProjectNumberGet(id string) (string, error)
ProjectIDSet(id string) error
// Compute Engine
RegionList(project, product string) ([]string, error)
ZoneList(project, region string) ([]string, error)
ImageLatestGet(project, imageproject, imagefamily string) (string, error)
MachineTypeList(project, zone string) (*compute.MachineTypeList, error)
MachineTypeFamilyList(imgs *compute.MachineTypeList) gcloud.LabeledValues
MachineTypeListByFamily(imgs *compute.MachineTypeList, family string) gcloud.LabeledValues
ImageList(project, imageproject string) (*compute.ImageList, error)
ImageTypeListByFamily(imgs *compute.ImageList, project, family string) gcloud.LabeledValues
ImageFamilyList(imgs *compute.ImageList) gcloud.LabeledValues
// Billing
BillingAccountList() ([]*cloudbilling.BillingAccount, error)
BillingAccountAttach(project, account string) error
// Domains
DomainIsAvailable(project, domain string) (*domainspb.RegisterParameters, error)
DomainIsVerified(project, domain string) (bool, error)
DomainRegister(project string, domaininfo *domainspb.RegisterParameters, contact gcloud.ContactData) error
// ServiceUsage
ServiceEnable(project string, service gcloud.Service) error
ServiceIsEnabled(project string, service gcloud.Service) (bool, error)
}
// Run takes a deploystack configuration and walks someone through all of the
// input needed to run the eventual terraform
func Run(s *config.Stack, useMock bool) {
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
}
defaultUserAgent := fmt.Sprintf("deploystack/%s", s.Config.Name)
client := gcloud.NewClient(context.Background(), defaultUserAgent)
q := NewQueue(s, &client)
if useMock {
q = NewQueue(s, GetMock(1))
}
q.InitializeUI()
p := tea.NewProgram(q.Start(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
Fatal(err)
}
if q.Get("halted") != nil {
Fatal(nil)
}
s.TerraformFile("terraform.tfvars")
fmt.Print("\n\n")
fmt.Print(titleStyle.Render("Deploystack"))
fmt.Print("\n")
fmt.Print(subTitleStyle.Render(s.Config.Title))
fmt.Print("\n")
fmt.Print(strong.Render("Installation will proceed with these settings"))
fmt.Print(q.getSettings())
}
// PreCheck handles presenting a choice to a user amongst multiple stacks
func PreCheck(reports []config.Report) string {
q := NewQueue(nil, GetMock(0))
q.Save("reports", reports)
appHeader := newHeader(appTitle, "Multiple Stacks Detected")
firstPage := newPicker("Please pick a stack to use", "Finding stacks", "stack", "", handleReports(&q))
firstPage.showProgress = false
firstPage.omitFromSettings = true
firstPage.addPostProcessor(handleStackSelection)
q.header = appHeader
q.add(&firstPage)
p := tea.NewProgram(q.Start(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
Fatal(err)
}
response := q.Get("stack").(string)
fmt.Print("\n\n")
fmt.Print(titleStyle.Render("Deploystack"))
fmt.Print("\n")
fmt.Print(subTitleStyle.Render("Stack has been chosen"))
fmt.Print("\n")
fmt.Print(strong.Render("Installation will proceed with this stack:"))
fmt.Print("\n")
fmt.Print(response)
fmt.Print("\n")
return response
}
// Fatal stops processing of Deploystack and halts the calling process. All
// with an eye towards not processing in the shell script of things go wrong.
func Fatal(err error) {
if err != nil {
content := `There was an issue collecting the information it takes to run this application.
You can try again by typing 'deploystack install' at the command prompt
If the issue persists, please report at:
https://github.com/GoogleCloudPlatform/deploystack/issues
`
errmsg := errMsg{
err: err,
usermsg: content,
quit: true,
}
msg := errorAlert{errmsg}
fmt.Print("\n\n")
fmt.Println(titleStyle.Render("DeployStack"))
fmt.Println(msg.Render())
}
fmt.Printf(clear)
os.Exit(1)
}