github.com/drycc/workflow-cli@v1.5.3-0.20240322092846-d4ee25983af9/cmd/apps.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/drycc/controller-sdk-go/api"
    13  	"github.com/drycc/controller-sdk-go/apps"
    14  	"github.com/drycc/controller-sdk-go/appsettings"
    15  	"github.com/drycc/controller-sdk-go/domains"
    16  	"github.com/drycc/controller-sdk-go/ps"
    17  	"github.com/drycc/workflow-cli/pkg/git"
    18  	"github.com/drycc/workflow-cli/pkg/logging"
    19  	"github.com/drycc/workflow-cli/pkg/webbrowser"
    20  	"github.com/drycc/workflow-cli/settings"
    21  	"golang.org/x/net/websocket"
    22  )
    23  
    24  // AppCreate creates an app.
    25  func (d *DryccCmd) AppCreate(id, remote string, noRemote bool) error {
    26  	s, err := settings.Load(d.ConfigFile)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	d.Print("Creating Application... ")
    32  	quit := progress(d.WOut)
    33  	app, err := apps.New(s.Client, id)
    34  
    35  	quit <- true
    36  	<-quit
    37  
    38  	if d.checkAPICompatibility(s.Client, err) != nil {
    39  		return err
    40  	}
    41  
    42  	d.Printf("done, created %s\n", app.ID)
    43  
    44  	if !noRemote {
    45  		if err = git.CreateRemote(git.DefaultCmd, s.Client.ControllerURL.Host, remote, app.ID); err != nil {
    46  			if strings.Contains(err.Error(), fmt.Sprintf("error: remote %s already exists.", remote)) {
    47  				msg := "A git remote with the name %s already exists. To overwrite this remote run:\n"
    48  				msg += "drycc git:remote --force --remote %s --app %s"
    49  				return fmt.Errorf(msg, remote, remote, app.ID)
    50  			}
    51  			return err
    52  		}
    53  
    54  		d.Printf(remoteCreationMsg, remote, app.ID)
    55  	}
    56  
    57  	if noRemote {
    58  		d.Printf("If you want to add a git remote for this app later, use `drycc git:remote -a %s`\n", app.ID)
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  // AppsList lists apps on the Drycc controller.
    65  func (d *DryccCmd) AppsList(results int) error {
    66  	s, err := settings.Load(d.ConfigFile)
    67  
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	if results == defaultLimit {
    73  		results = s.Limit
    74  	}
    75  
    76  	apps, count, err := apps.List(s.Client, results)
    77  	if d.checkAPICompatibility(s.Client, err) != nil {
    78  		return err
    79  	}
    80  	if count > 0 {
    81  		table := d.getDefaultFormatTable([]string{"ID", "UUID", "OWNER", "CREATED", "UPDATED"})
    82  		for _, app := range apps {
    83  			table.Append([]string{
    84  				app.ID,
    85  				app.UUID,
    86  				app.Owner,
    87  				app.Created,
    88  				app.Updated,
    89  			})
    90  		}
    91  		table.Render()
    92  	} else {
    93  		d.Println("No apps found.")
    94  	}
    95  	return nil
    96  }
    97  
    98  // AppInfo prints info about app.
    99  func (d *DryccCmd) AppInfo(appID string) error {
   100  	s, appID, err := load(d.ConfigFile, appID)
   101  
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	app, err := apps.Get(s.Client, appID)
   107  	if d.checkAPICompatibility(s.Client, err) != nil {
   108  		return err
   109  	}
   110  
   111  	url, err := d.appURL(s, appID)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	table := d.getDefaultFormatTable([]string{})
   117  	table.Append([]string{"App:", app.ID})
   118  	table.Append([]string{"URL:", url})
   119  	table.Append([]string{"UUID:", app.UUID})
   120  	table.Append([]string{"Owner:", app.Owner})
   121  	table.Append([]string{"Created:", app.Created})
   122  	table.Append([]string{"Updated:", app.Updated})
   123  
   124  	// print the app processes
   125  	processes, _, err := ps.List(s.Client, appID, defaultLimit)
   126  	if d.checkAPICompatibility(s.Client, err) != nil {
   127  		return err
   128  	}
   129  
   130  	if len(processes) > 0 {
   131  		table.Append([]string{"Processes:"})
   132  		for index, process := range processes {
   133  			table.Append([]string{"", "Name:", process.Name})
   134  			table.Append([]string{"", "Release:", process.Release})
   135  			table.Append([]string{"", "State:", process.State})
   136  			table.Append([]string{"", "Type:", process.Type})
   137  			table.Append([]string{"", "Started:", process.Started.Format("2006-01-02T15:04:05MST")})
   138  			if len(processes) > index+1 {
   139  				table.Append([]string{""})
   140  			}
   141  		}
   142  	} else {
   143  		table.Append([]string{"Processes:", safeGetString("")})
   144  	}
   145  
   146  	domains, _, err := domains.List(s.Client, appID, defaultLimit)
   147  	if d.checkAPICompatibility(s.Client, err) != nil {
   148  		return err
   149  	}
   150  	if len(domains) > 0 {
   151  		table.Append([]string{"Domains:"})
   152  		for index, domain := range domains {
   153  			table.Append([]string{"", "Domain:", domain.Domain})
   154  			table.Append([]string{"", "Created:", domain.Created})
   155  			table.Append([]string{"", "Updated:", domain.Updated})
   156  			if len(domains) > index+1 {
   157  				table.Append([]string{""})
   158  			}
   159  		}
   160  	} else {
   161  		table.Append([]string{"Domains:", safeGetString("")})
   162  	}
   163  
   164  	appSettings, err := appsettings.List(s.Client, appID)
   165  	if d.checkAPICompatibility(s.Client, err) != nil {
   166  		return err
   167  	}
   168  	if len(appSettings.Label) > 0 {
   169  		table.Append([]string{"Labels:"})
   170  		for index, label := range *sortKeys(appSettings.Label) {
   171  			table.Append([]string{"", "Key:", label})
   172  			table.Append([]string{"", "Value:", fmt.Sprintf("%v", appSettings.Label[label])})
   173  			if len(appSettings.Label) > index+1 {
   174  				table.Append([]string{""})
   175  			}
   176  		}
   177  	} else {
   178  		table.Append([]string{"Labels:", safeGetString("")})
   179  	}
   180  	table.Render()
   181  	return nil
   182  }
   183  
   184  // AppOpen opens an app in the default webbrowser.
   185  func (d *DryccCmd) AppOpen(appID string) error {
   186  	s, appID, err := load(d.ConfigFile, appID)
   187  
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	u, err := d.appURL(s, appID)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	if u == "" {
   198  		return fmt.Errorf(noDomainAssignedMsg, appID)
   199  	}
   200  
   201  	if !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) {
   202  		u = "http://" + u
   203  	}
   204  
   205  	return webbrowser.Webbrowser(u)
   206  }
   207  
   208  // AppLogs returns the logs from an app.
   209  func (d *DryccCmd) AppLogs(appID string, lines int, follow bool, timeout int) error {
   210  	s, appID, err := load(d.ConfigFile, appID)
   211  
   212  	if err != nil {
   213  		return err
   214  	}
   215  	request := api.AppLogsRequest{
   216  		Lines:   lines,
   217  		Follow:  follow,
   218  		Timeout: timeout,
   219  	}
   220  	conn, err := apps.Logs(s.Client, appID, request)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	defer conn.Close()
   225  	for {
   226  		var message string
   227  		err := websocket.Message.Receive(conn, &message)
   228  		if err != nil {
   229  			if err != io.EOF {
   230  				log.Printf("error: %v", err)
   231  			}
   232  			break
   233  		}
   234  		logging.PrintLog(os.Stdout, strings.TrimRight(string(message), "\n"))
   235  	}
   236  	return nil
   237  }
   238  
   239  // AppRun runs a one time command in the app.
   240  func (d *DryccCmd) AppRun(appID, command string, volumeVars []string, timeout, expires uint32) error {
   241  	s, appID, err := load(d.ConfigFile, appID)
   242  
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	d.Printf("Running '%s'...\n", command)
   248  	volumeMap, err := parseMount(volumeVars)
   249  	if d.checkAPICompatibility(s.Client, err) != nil {
   250  		return err
   251  	}
   252  
   253  	if err := apps.Run(s.Client, appID, command, volumeMap, timeout, expires); d.checkAPICompatibility(s.Client, err) != nil {
   254  		return err
   255  	}
   256  	return nil
   257  }
   258  
   259  func parseMount(volumeVars []string) (map[string]interface{}, error) {
   260  	volumeMap := make(map[string]interface{})
   261  
   262  	regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*):([\s\S]*)$`)
   263  	for _, volume := range volumeVars {
   264  		if regex.MatchString(volume) {
   265  			captures := regex.FindStringSubmatch(volume)
   266  			volumeMap[captures[1]] = captures[2]
   267  		} else {
   268  			return nil, fmt.Errorf("'%s' does not match the pattern 'key:var', ex: MODE:test", volume)
   269  		}
   270  	}
   271  	return volumeMap, nil
   272  }
   273  
   274  // AppDestroy destroys an app.
   275  func (d *DryccCmd) AppDestroy(appID, confirm string) error {
   276  	gitSession := false
   277  
   278  	s, err := settings.Load(d.ConfigFile)
   279  
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	if appID == "" {
   285  		appID, err = git.DetectAppName(git.DefaultCmd, s.Client.ControllerURL.Host)
   286  
   287  		if err != nil {
   288  			return err
   289  		}
   290  
   291  		gitSession = true
   292  	}
   293  
   294  	if confirm == "" {
   295  		d.Printf(` !    WARNING: Potentially Destructive Action
   296   !    This command will destroy the application: %s
   297   !    To proceed, type "%s" or re-run this command with --confirm=%s
   298  
   299  > `, appID, appID, appID)
   300  
   301  		fmt.Scanln(&confirm)
   302  	}
   303  
   304  	if confirm != appID {
   305  		return fmt.Errorf("app %s does not match confirm %s, aborting", appID, confirm)
   306  	}
   307  
   308  	startTime := time.Now()
   309  	d.Printf("Destroying %s...\n", appID)
   310  
   311  	if err = apps.Delete(s.Client, appID); d.checkAPICompatibility(s.Client, err) != nil {
   312  		return err
   313  	}
   314  
   315  	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
   316  
   317  	if gitSession {
   318  		return d.GitRemove(appID)
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  // AppTransfer transfers app ownership to another user.
   325  func (d *DryccCmd) AppTransfer(appID, username string) error {
   326  	s, appID, err := load(d.ConfigFile, appID)
   327  
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	d.Printf("Transferring %s to %s... ", appID, username)
   333  
   334  	err = apps.Transfer(s.Client, appID, username)
   335  	if d.checkAPICompatibility(s.Client, err) != nil {
   336  		return err
   337  	}
   338  
   339  	d.Println("done")
   340  
   341  	return nil
   342  }
   343  
   344  const noDomainAssignedMsg = "no domain assigned to %s"
   345  
   346  // appURL grabs the first domain an app has and returns this.
   347  func (d *DryccCmd) appURL(s *settings.Settings, appID string) (string, error) {
   348  	domains, _, err := domains.List(s.Client, appID, 1)
   349  	if d.checkAPICompatibility(s.Client, err) != nil {
   350  		return "", err
   351  	}
   352  
   353  	if len(domains) == 0 {
   354  		return "", nil
   355  	}
   356  
   357  	return expandURL(s.Client.ControllerURL.Host, domains[0].Domain), nil
   358  }
   359  
   360  // expandURL expands an app url if necessary.
   361  func expandURL(host, u string) string {
   362  	if strings.Contains(u, ".") {
   363  		// If domain is a full url.
   364  		return u
   365  	}
   366  
   367  	// If domain is a subdomain, look up the controller url and replace the subdomain.
   368  	parts := strings.Split(host, ".")
   369  	parts[0] = u
   370  	return strings.Join(parts, ".")
   371  }