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 }