Skip to content
This repository has been archived by the owner on Dec 4, 2018. It is now read-only.

Commit

Permalink
Add ability to tail logs during app start
Browse files Browse the repository at this point in the history
Fix situations where CI has failed, and -venerable apps left dangling

May fix some of the edge cases identified in <#37 (comment)>
  • Loading branch information
aeijdenberg authored and xoebus committed Feb 1, 2018
1 parent 20c2340 commit aea62a3
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 95 deletions.
210 changes: 159 additions & 51 deletions autopilot.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/url"
"os"
"strings"
"time"

"code.cloudfoundry.org/cli/cf/api/logs"
"code.cloudfoundry.org/cli/plugin"
"github.com/cloudfoundry/noaa/consumer"
"github.com/contraband/autopilot/rewind"
)

Expand All @@ -30,74 +35,119 @@ func venerableAppName(appName string) string {
return fmt.Sprintf("%s-venerable", appName)
}

func getActionsForExistingApp(appRepo *ApplicationRepo, appName, manifestPath, appPath string) []rewind.Action {
func getActionsForApp(appRepo *ApplicationRepo, appName, manifestPath, appPath string, showLogs bool) []rewind.Action {
venName := venerableAppName(appName)
var err error
var curApp, venApp *AppEntity
var haveVenToCleanup bool

return []rewind.Action{
// rename
// get info about current app
{
Forward: func() error {
curApp, err = appRepo.GetAppMetadata(appName)
if err != ErrAppNotFound {
return err
}
curApp = nil
return nil
},
},
// get info about ven app
{
Forward: func() error {
return appRepo.RenameApplication(appName, venerableAppName(appName))
venApp, err = appRepo.GetAppMetadata(venName)
if err != ErrAppNotFound {
return err
}
venApp = nil
return nil
},
},
// rename any existing app such so that next step can push to a clear space
{
Forward: func() error {
// Unless otherwise specified, go with our start state
haveVenToCleanup = (venApp != nil)

// If there is no current app running, that's great, we're done here
if curApp == nil {
return nil
}

// If current app isn't started, then we'll just delete it, and we're done
if curApp.State != "STARTED" {
return appRepo.DeleteApplication(appName)
}

// Do we have a ven app that will stop a rename?
if venApp != nil {
// Finally, since the current app claims to be healthy, we'll delete the venerable app, and rename the current over the top
err = appRepo.DeleteApplication(venName)
if err != nil {
return err
}
}

// Finally, rename
haveVenToCleanup = true
return appRepo.RenameApplication(appName, venName)
},
},
// push
{
Forward: func() error {
return appRepo.PushApplication(appName, manifestPath, appPath)
return appRepo.PushApplication(appName, manifestPath, appPath, showLogs)
},
ReversePrevious: func() error {
if !haveVenToCleanup {
return nil
}

// If the app cannot start we'll have a lingering application
// We delete this application so that the rename can succeed
appRepo.DeleteApplication(appName)

return appRepo.RenameApplication(venerableAppName(appName), appName)
return appRepo.RenameApplication(venName, appName)
},
},
// delete
{
Forward: func() error {
return appRepo.DeleteApplication(venerableAppName(appName))
if !haveVenToCleanup {
return nil
}
return appRepo.DeleteApplication(venName)
},
},
}
}

func getActionsForNewApp(appRepo *ApplicationRepo, appName, manifestPath, appPath string) []rewind.Action {
func getActionsForNewApp(appRepo *ApplicationRepo, appName, manifestPath, appPath string, showLogs bool) []rewind.Action {
return []rewind.Action{
// push
{
Forward: func() error {
return appRepo.PushApplication(appName, manifestPath, appPath)
return appRepo.PushApplication(appName, manifestPath, appPath, showLogs)
},
},
}
}

func (plugin AutopilotPlugin) Run(cliConnection plugin.CliConnection, args []string) {
if len(args) > 0 && args[0] == "CLI-MESSAGE-UNINSTALL" {
// only handle if actually invoked, else it can't be uninstalled cleanly
if args[0] != "zero-downtime-push" {
return
}

appRepo := NewApplicationRepo(cliConnection)
appName, manifestPath, appPath, err := ParseArgs(args)
appName, manifestPath, appPath, showLogs, err := ParseArgs(args)
fatalIf(err)

appExists, err := appRepo.DoesAppExist(appName)
fatalIf(err)

var actionList []rewind.Action

if appExists {
actionList = getActionsForExistingApp(appRepo, appName, manifestPath, appPath)
} else {
actionList = getActionsForNewApp(appRepo, appName, manifestPath, appPath)
}

actions := rewind.Actions{
Actions: actionList,
fatalIf((&rewind.Actions{
Actions: getActionsForApp(appRepo, appName, manifestPath, appPath, showLogs),
RewindFailureMessage: "Oh no. Something's gone wrong. I've tried to roll back but you should check to see if everything is OK.",
}

err = actions.Execute()
fatalIf(err)
}).Execute())

fmt.Println()
fmt.Println("A new version of your application has successfully been pushed!")
Expand Down Expand Up @@ -126,26 +176,33 @@ func (AutopilotPlugin) GetMetadata() plugin.PluginMetadata {
}
}

func ParseArgs(args []string) (string, string, string, error) {
func ParseArgs(args []string) (string, string, string, bool, error) {
flags := flag.NewFlagSet("zero-downtime-push", flag.ContinueOnError)
manifestPath := flags.String("f", "", "path to an application manifest")
appPath := flags.String("p", "", "path to application files")
showLogs := flags.Bool("show-app-log", false, "tail and show application log during application start")

if len(args) < 2 || strings.HasPrefix(args[1], "-") {
return "", "", "", false, ErrNoArgs
}
err := flags.Parse(args[2:])
if err != nil {
return "", "", "", err
return "", "", "", false, err
}

appName := args[1]

if *manifestPath == "" {
return "", "", "", ErrNoManifest
return "", "", "", false, ErrNoManifest
}

return appName, *manifestPath, *appPath, nil
return appName, *manifestPath, *appPath, *showLogs, nil
}

var ErrNoManifest = errors.New("a manifest is required to push this application")
var (
ErrNoArgs = errors.New("app name must be specified")
ErrNoManifest = errors.New("a manifest is required to push this application")
)

type ApplicationRepo struct {
conn plugin.CliConnection
Expand All @@ -162,15 +219,61 @@ func (repo *ApplicationRepo) RenameApplication(oldName, newName string) error {
return err
}

func (repo *ApplicationRepo) PushApplication(appName, manifestPath, appPath string) error {
args := []string{"push", appName, "-f", manifestPath}
func (repo *ApplicationRepo) PushApplication(appName, manifestPath, appPath string, showLogs bool) error {
args := []string{"push", appName, "-f", manifestPath, "--no-start"}

if appPath != "" {
args = append(args, "-p", appPath)
}

_, err := repo.conn.CliCommand(args...)
return err
if err != nil {
return err
}

if showLogs {
app, err := repo.conn.GetApp(appName)
if err != nil {
return err
}
dopplerEndpoint, err := repo.conn.DopplerEndpoint()
if err != nil {
return err
}
token, err := repo.conn.AccessToken()
if err != nil {
return err
}

cons := consumer.New(dopplerEndpoint, nil, nil)
defer cons.Close()

messages, errors := cons.TailingLogs(app.Guid, token)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
for {
select {
case m := <-messages:
if m.GetSourceType() != "STG" { // skip STG messages as the cf tool already prints them
os.Stderr.WriteString(logs.NewNoaaLogMessage(m).ToLog(time.Local) + "\n")
}
case e := <-errors:
log.Println("error reading logs:", e)
case <-ctx.Done():
return
}
}
}()
}

_, err = repo.conn.CliCommand("start", appName)
if err != nil {
return err
}

return nil
}

func (repo *ApplicationRepo) DeleteApplication(appName string) error {
Expand All @@ -183,39 +286,44 @@ func (repo *ApplicationRepo) ListApplications() error {
return err
}

func (repo *ApplicationRepo) DoesAppExist(appName string) (bool, error) {
type AppEntity struct {
State string `json:"state"`
}

var (
ErrAppNotFound = errors.New("application not found")
)

// GetAppMetadata returns metadata about an app with appName
func (repo *ApplicationRepo) GetAppMetadata(appName string) (*AppEntity, error) {
space, err := repo.conn.GetCurrentSpace()
if err != nil {
return false, err
return nil, err
}

path := fmt.Sprintf(`v2/apps?q=name:%s&q=space_guid:%s`, url.QueryEscape(appName), space.Guid)
result, err := repo.conn.CliCommandWithoutTerminalOutput("curl", path)

if err != nil {
return false, err
return nil, err
}

jsonResp := strings.Join(result, "")

output := make(map[string]interface{})
output := struct {
Resources []struct {
Entity AppEntity `json:"entity"`
} `json:"resources"`
}{}
err = json.Unmarshal([]byte(jsonResp), &output)

if err != nil {
return false, err
return nil, err
}

totalResults, ok := output["total_results"]

if !ok {
return false, errors.New("Missing total_results from api response")
}

count, ok := totalResults.(float64)

if !ok {
return false, fmt.Errorf("total_results didn't have a number %v", totalResults)
if len(output.Resources) == 0 {
return nil, ErrAppNotFound
}

return count == 1, nil
return &output.Resources[0].Entity, nil
}
Loading

0 comments on commit aea62a3

Please sign in to comment.