Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add app commands #248

Merged
merged 6 commits into from
Mar 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions awss3/awss3.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package awss3

import (
"context"
"path/filepath"

"github.com/TouchBistro/tb/util"
"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -41,7 +40,7 @@ func ListObjectKeysByPrefix(bucket, objKeyPrefix string) ([]string, error) {
return keys, nil
}

func DownloadObject(bucket, objKey, dstDir string) error {
func DownloadObject(bucket, objKey, dstPath string) error {
// Set up AWS Env Vars
conf, err := external.LoadDefaultAWSConfig()
if err != nil {
Expand All @@ -62,13 +61,12 @@ func DownloadObject(bucket, objKey, dstDir string) error {
defer resp.Body.Close()

// Download to a local file.
dlPath := filepath.Join(dstDir, objKey)
nBytes, err := util.DownloadFile(dlPath, resp.Body)
nBytes, err := util.DownloadFile(dstPath, resp.Body)
if err != nil {
return errors.Wrapf(err, "failed downloading file to %s", dlPath)
return errors.Wrapf(err, "failed downloading file to %s", dstPath)
}

log.Debugf("Wrote %d bytes to %s successfully", nBytes, dlPath)
log.Debugf("Wrote %d bytes to %s successfully", nBytes, dstPath)

return nil
}
166 changes: 166 additions & 0 deletions cmd/app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package app

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/TouchBistro/goutils/fatal"
"github.com/TouchBistro/goutils/spinner"
"github.com/TouchBistro/tb/app"
"github.com/TouchBistro/tb/awss3"
"github.com/TouchBistro/tb/config"
"github.com/TouchBistro/tb/git"
"github.com/TouchBistro/tb/simulator"
"github.com/TouchBistro/tb/util"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var appCmd = &cobra.Command{
Use: "app",
Short: "tb app allows running and managing different kinds of applications",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Put app specific configuration & setup logic here

// Check if current command is an ios subcommand
isIOSCommand := cmd.Parent().Name() == "ios"

if isIOSCommand && runtime.GOOS != "darwin" {
fatal.Exit("Error: tb app ios is only supported on macOS")
}

// Get global flag value
noRegistryPull, err := cmd.Flags().GetBool("no-registry-pull")
if err != nil {
// This is a coding error
fatal.ExitErr(err, "failed to get flag")
}

// Need to do this explicitly here since we are defining PersistentPreRun
// PersistentPreRun overrides the parent command's one if defined, so the one in root won't be run.
err = config.Init(config.InitOptions{
UpdateRegistries: !noRegistryPull,
LoadServices: false,
LoadApps: true,
})
if err != nil {
fatal.ExitErr(err, "Failed to initialize config files")
}

if isIOSCommand {
err = simulator.LoadSimulators()
if err != nil {
fatal.ExitErr(err, "Failed to find available iOS simulators")
}
}
},
}

func AppCmd() *cobra.Command {
return appCmd
}

func DownloadLatestApp(a app.App, downloadDest string) string {
// Look up the latest build sha for user-specified branch and app.
s3Dir := filepath.Join(a.Name, a.Branch)
log.Infof("Checking objects on aws in bucket %s matching prefix %s...", a.Storage.Bucket, s3Dir)
s3Builds, err := awss3.ListObjectKeysByPrefix(a.Storage.Bucket, s3Dir)
if err != nil {
fatal.ExitErrf(err, "Failed getting keys from s3 in dir %s", s3Dir)
}
if len(s3Builds) == 0 {
fatal.Exitf("could not find any builds for %s", s3Dir)
} else if len(s3Builds) > 1 {
// We only expect one build per branch. If we find two, its likely a bug or some kind of
// race condition from the build-uploading side.
// If this gets clunky we can determine a sort order for the builds.
fatal.Exitf("Got the following builds for this branch %+v. Only expecting one build", s3Builds)
}

pathToS3Tarball := s3Builds[0]
s3BuildFilename := filepath.Base(pathToS3Tarball)

// Decide whether or not to pull down a new version.

localBranchDir := filepath.Join(downloadDest, a.FullName(), a.Branch)
log.Infof("Checking contents at %s to see if we need to download a new version from S3", localBranchDir)

pattern := fmt.Sprintf("%s/*.app", localBranchDir)
localBuilds, err := filepath.Glob(pattern)
if err != nil {
fatal.ExitErrf(err, "couldn't glob for %s", pattern)
}

if len(localBuilds) > 1 {
fatal.Exitf("Got the following builds: %+v. Only expecting one build", localBuilds)
}

// If there is a local build, compare its sha against s3 and github versions
var refreshLocalBuild bool
if len(localBuilds) == 1 {
localBuild := localBuilds[0]

// If there is a local build, get latest sha from github for desired branch to see if the build available on s3 corresponds to the
// latest commit on the branch.
log.Infof("Checking latest github sha for %s-%s", a.GitRepo, a.Branch)
latestGitsha, err := git.GetBranchHeadSha(a.GitRepo, a.Branch)
if err != nil {
fatal.ExitErrf(err, "Failed getting branch head sha for %s-%s", a.GitRepo, a.Branch)
}
log.Infof("Latest github sha is %s", latestGitsha)
if !strings.HasPrefix(s3BuildFilename, latestGitsha) {
log.Warnf("sha of s3 build %s does not match latest github sha %s for branch %s", s3BuildFilename, latestGitsha, a.Branch)
}

currentSha := strings.Split(filepath.Base(localBuild), ".")[0]
s3Sha := strings.Split(s3BuildFilename, ".")[0]

log.Infof("Current local build sha is %s", currentSha)
log.Infof("Latest s3 sha is %s", s3Sha)

if currentSha == s3Sha {
log.Infoln("Current build sha matches remote sha")
} else {
log.Infoln("Current build sha is different from s3 sha. Deleting local version...")
err := os.RemoveAll(localBranchDir)
if err != nil {
fatal.ExitErrf(err, "failed to delete %s", localBranchDir)
}

refreshLocalBuild = true
}
}

// Path where the downloaded app is
dstPath := filepath.Join(downloadDest, a.FullName(), a.Branch, s3BuildFilename)

// If there are no local builds or if our local build was deemed out of date, download the latest object from S3
if len(localBuilds) == 0 || refreshLocalBuild {
log.Infof("Downloading %s from bucket %s to %s", pathToS3Tarball, a.Storage.Bucket, downloadDest)
successCh := make(chan string)
failedCh := make(chan error)
go func(successCh chan string, failedCh chan error) {
err = awss3.DownloadObject(a.Storage.Bucket, pathToS3Tarball, dstPath)
if err != nil {
failedCh <- errors.Wrapf(err, "Failed to download a file from s3 from %s to %s", pathToS3Tarball, downloadDest)
return
}
successCh <- pathToS3Tarball
}(successCh, failedCh)
count := 1
spinner.SpinnerWait(successCh, failedCh, "\t☑ finished downloading %s\n", "failed S3 download", count)

// Untar, ungzip and cleanup the file
log.Infof("untar-ing %s", dstPath)
err := util.Untar(dstPath, true)
if err != nil {
fatal.ExitErrf(err, "Failed to untar or cleanup app archive at %s", dstPath)
}
}

return strings.TrimSuffix(dstPath, ".tgz")
}
14 changes: 14 additions & 0 deletions cmd/app/desktop/desktop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package desktop

import (
"github.com/spf13/cobra"
)

var desktopCmd = &cobra.Command{
Use: "desktop",
Short: "tb app desktop allows running and managing desktop applications",
}

func DesktopCmd() *cobra.Command {
return desktopCmd
}
93 changes: 93 additions & 0 deletions cmd/app/desktop/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package desktop

import (
"errors"
"os"
"runtime"

"github.com/TouchBistro/goutils/command"
"github.com/TouchBistro/goutils/fatal"
"github.com/TouchBistro/goutils/file"
appCmd "github.com/TouchBistro/tb/cmd/app"
"github.com/TouchBistro/tb/config"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type runOptions struct {
branch string
}

var runOpts runOptions

var runCmd = &cobra.Command{
Use: "run",
Args: func(cmd *cobra.Command, args []string) error {
// Verify that the app name was provided as a single arg
if len(args) < 1 {
return errors.New("app name is required as an argument")
} else if len(args) > 1 {
return errors.New("only one argument is accepted")
}

return nil
},
Short: "Runs a desktop application",
Long: `Runs a desktop application.

Examples:
- run the current master build of TouchBistroServer
tb app desktop run TouchBistroServer

- run the build for a specific branch
tb app desktop run TouchBistroServer --branch task/bug-631/fix-thing`,
Run: func(cmd *cobra.Command, args []string) {
appName := args[0]
a, err := config.LoadedDesktopApps().Get(appName)
if err != nil {
fatal.ExitErrf(err, "%s is not a valid desktop app\n", appName)
}

// Override branch if one was provided
if runOpts.branch != "" {
a.Branch = runOpts.branch
}

downloadDest := config.DesktopAppsPath()
// Check disk utilisation by desktop directory
usageBytes, err := file.DirSize(downloadDest)
if err != nil {
fatal.ExitErr(err, "Error checking ios build disk space usage")
}
log.Infof("Current desktop app build disk usage: %.2fGB", float64(usageBytes)/1024.0/1024.0/1024.0)

appPath := appCmd.DownloadLatestApp(a, downloadDest)

// Set env vars so they are available in the app process
for k, v := range a.EnvVars {
log.Debugf("Setting %s to %s", k, v)
os.Setenv(k, v)
}

log.Info("☐ Launching app")

// TODO probably want to figure out a better way to abstract opening an app cross platform
if runtime.GOOS == "darwin" {
err = command.Exec("open", []string{appPath}, "tb-app-desktop-run-open")
} else {
fatal.Exit("tb app desktop run is not supported on your platform")
}

if err != nil {
fatal.ExitErrf(err, "failed to run app %s", a.FullName())
}

log.Info("☑ Launched app")
log.Info("🎉🎉🎉 Enjoy!")
},
}

func init() {
desktopCmd.AddCommand(runCmd)
runCmd.Flags().StringVarP(&runOpts.branch, "branch", "b", "", "The name of the git branch associated build to pull down and run")
}
14 changes: 14 additions & 0 deletions cmd/app/ios/ios.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ios

import (
"github.com/spf13/cobra"
)

var iosCmd = &cobra.Command{
Use: "ios",
Short: "tb app ios allows running and managing iOS apps",
}

func IOSCmd() *cobra.Command {
return iosCmd
}
67 changes: 67 additions & 0 deletions cmd/app/ios/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ios

import (
"os"
"os/exec"
"path/filepath"

"github.com/TouchBistro/goutils/command"
"github.com/TouchBistro/goutils/fatal"
"github.com/TouchBistro/tb/simulator"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

type logsOptions struct {
iosVersion string
deviceName string
numberOfLines string
}

var logOpts logsOptions

var logsCmd = &cobra.Command{
Use: "logs",
Short: "Displays logs from the given simulator",
Long: `Displays logs from the given simulator.

Examples:
- displays the last 10 logs in the default iOS simulator
tb app logs

- displays the last 20 logs in an iOS 12.4 iPad Air 2 simulator
tb app logs --number 20 --ios-version 12.4 --device iPad Air 2`,
Run: func(cmd *cobra.Command, args []string) {
if logOpts.iosVersion == "" {
logOpts.iosVersion = simulator.GetLatestIOSVersion()
log.Infof("No iOS version provided, defaulting to version %s\n", logOpts.iosVersion)
}

log.Debugln("☐ Finding device UDID")

deviceUDID, err := simulator.GetDeviceUDID("iOS "+logOpts.iosVersion, logOpts.deviceName)
if err != nil {
fatal.ExitErr(err, "☒ Failed to get device UUID.\nRun \"xcrun simctl list devices\" to list available simulators.")
}

log.Debugf("☑ Found device UDID: %s\n", deviceUDID)

logsPath := filepath.Join(os.Getenv("HOME"), "Library/Logs/CoreSimulator", deviceUDID, "system.log")
log.Infof("Attaching to logs for simulator %s\n\n", logOpts.deviceName)

err = command.Exec("tail", []string{"-f", "-n", logOpts.numberOfLines, logsPath}, "ios-logs-tail", func(cmd *exec.Cmd) {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
})
if err != nil {
fatal.ExitErrf(err, "Failed to get logs for simulator %s with iOS version %s", logOpts.deviceName, logOpts.iosVersion)
}
},
}

func init() {
iosCmd.AddCommand(logsCmd)
logsCmd.Flags().StringVarP(&logOpts.iosVersion, "ios-version", "i", "", "The iOS version to use")
logsCmd.Flags().StringVarP(&logOpts.deviceName, "device", "d", "iPad Air (3rd generation)", "The name of the device to use")
logsCmd.Flags().StringVarP(&logOpts.numberOfLines, "number", "n", "10", "The number of lines to display")
}
Loading