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  }