-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli):
numerous status
command for displaying app workload status
- Loading branch information
Showing
9 changed files
with
571 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package status | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/spf13/cobra" | ||
"numerous.com/cli/cmd/args" | ||
"numerous.com/cli/cmd/errorhandling" | ||
"numerous.com/cli/cmd/usage" | ||
"numerous.com/cli/internal/app" | ||
"numerous.com/cli/internal/gql" | ||
) | ||
|
||
const cmdActionText = "to see the status of" | ||
|
||
const longFormat = `Get an overview of the status of all workloads related to an app. | ||
%s | ||
%s | ||
` | ||
|
||
var long = fmt.Sprintf(longFormat, usage.AppIdentifier(cmdActionText), usage.AppDirectoryArgument) | ||
|
||
var cmdArgs struct { | ||
orgSlug string | ||
appSlug string | ||
appDir string | ||
} | ||
|
||
var Cmd = &cobra.Command{ | ||
Use: "status", | ||
Short: "Get the status of an apps workloads", | ||
Long: long, | ||
Args: args.OptionalAppDir(&cmdArgs.appDir), | ||
RunE: run, | ||
} | ||
|
||
func run(cmd *cobra.Command, args []string) error { | ||
service := app.New(gql.NewClient(), nil, http.DefaultClient) | ||
|
||
input := statusInput{ | ||
appDir: cmdArgs.appDir, | ||
appSlug: cmdArgs.appSlug, | ||
orgSlug: cmdArgs.orgSlug, | ||
} | ||
err := status(cmd.Context(), service, input) | ||
|
||
return errorhandling.ErrorAlreadyPrinted(err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package status | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/stretchr/testify/mock" | ||
"numerous.com/cli/internal/app" | ||
) | ||
|
||
var _ appReaderWorkloadLister = &mockAppReaderWorkloadLister{} | ||
|
||
type mockAppReaderWorkloadLister struct{ mock.Mock } | ||
|
||
// ListAppWorkloads implements appReaderWorkloadLister. | ||
func (m *mockAppReaderWorkloadLister) ListAppWorkloads(ctx context.Context, input app.ListAppWorkloadsInput) ([]app.AppWorkload, error) { | ||
args := m.Called(ctx, input) | ||
return args.Get(0).([]app.AppWorkload), args.Error(1) | ||
} | ||
|
||
// ReadApp implements appReaderWorkloadLister. | ||
func (m *mockAppReaderWorkloadLister) ReadApp(ctx context.Context, input app.ReadAppInput) (app.ReadAppOutput, error) { | ||
args := m.Called(ctx, input) | ||
return args.Get(0).(app.ReadAppOutput), args.Error(1) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package status | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"math" | ||
"time" | ||
|
||
"numerous.com/cli/internal/app" | ||
"numerous.com/cli/internal/appident" | ||
) | ||
|
||
type statusInput struct { | ||
appDir string | ||
appSlug string | ||
orgSlug string | ||
} | ||
|
||
type appReaderWorkloadLister interface { | ||
ReadApp(ctx context.Context, input app.ReadAppInput) (app.ReadAppOutput, error) | ||
ListAppWorkloads(ctx context.Context, input app.ListAppWorkloadsInput) ([]app.AppWorkload, error) | ||
} | ||
|
||
func status(ctx context.Context, apps appReaderWorkloadLister, input statusInput) error { | ||
ai, err := appident.GetAppIdentifier(input.appDir, nil, input.orgSlug, input.appSlug) | ||
if err != nil { | ||
appident.PrintGetAppIdentifierError(err, input.appDir, ai) | ||
return err | ||
} | ||
|
||
readOutput, err := apps.ReadApp(ctx, app.ReadAppInput{OrganizationSlug: ai.OrganizationSlug, AppSlug: ai.AppSlug}) | ||
if err != nil { | ||
app.PrintAppError(err, ai) | ||
return err | ||
} | ||
|
||
workloads, err := apps.ListAppWorkloads(ctx, app.ListAppWorkloadsInput(readOutput)) | ||
if err != nil { | ||
app.PrintAppError(err, ai) | ||
return err | ||
} | ||
|
||
printWorkloads(workloads) | ||
|
||
return nil | ||
} | ||
|
||
func printWorkloads(workloads []app.AppWorkload) { | ||
first := true | ||
for _, w := range workloads { | ||
if !first { | ||
println() | ||
} | ||
printWorkload(w) | ||
first = false | ||
} | ||
} | ||
|
||
func printWorkload(workload app.AppWorkload) { | ||
if workload.OrganizationSlug != "" { | ||
fmt.Printf("Workload in %q:\n", workload.OrganizationSlug) | ||
} else if sub := workload.Subscription; sub != nil { | ||
fmt.Printf("Workload for subscription %q in %q:\n", sub.SubscriptionUUID, sub.OrganizationSlug) | ||
} else { | ||
fmt.Println("Workload of unknown origin:") | ||
} | ||
|
||
fmt.Printf(" Status: %s\n", workload.Status) | ||
fmt.Printf(" Started at: %s (up for %s)\n", workload.StartedAt.Format(time.DateTime), humanDuration(time.Since(workload.StartedAt))) | ||
} | ||
|
||
const ( | ||
hoursPerDay float64 = 24.0 | ||
minutesPerHour float64 = 60.0 | ||
secondsPerMinute float64 = 60.0 | ||
) | ||
|
||
func humanDuration(since time.Duration) string { | ||
hours := since.Hours() | ||
if hours > hoursPerDay { | ||
fullDays := math.Floor(hours / hoursPerDay) | ||
dayHours := math.Floor(hours - fullDays*hoursPerDay) | ||
if dayHours > 0.0 { | ||
return fmt.Sprintf("%d days and %d hours", int(fullDays), int(dayHours)) | ||
} else { | ||
return fmt.Sprintf("%d days", int(fullDays)) | ||
} | ||
} | ||
|
||
minutes := since.Minutes() | ||
if hours > 1.0 { | ||
fullHours := math.Floor(hours) | ||
hourMinutes := minutes - fullHours*minutesPerHour | ||
if fullHours > 0.0 { | ||
return fmt.Sprintf("%d hours and %d minutes", int(fullHours), int(hourMinutes)) | ||
} | ||
} | ||
|
||
seconds := since.Seconds() | ||
if minutes > 1.0 { | ||
fullMinutes := math.Floor(minutes) | ||
minuteSeconds := seconds - fullMinutes*secondsPerMinute | ||
if fullMinutes > 0.0 { | ||
return fmt.Sprintf("%d minutes and %d seconds", int(fullMinutes), int(minuteSeconds)) | ||
} | ||
} | ||
|
||
return fmt.Sprintf("%d seconds", int(math.Round(seconds))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package status | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
"numerous.com/cli/internal/app" | ||
) | ||
|
||
var errTest = errors.New("test error") | ||
|
||
func TestStatus(t *testing.T) { | ||
ctx := context.Background() | ||
appID := "test-app-id" | ||
appSlug := "test-app-slug" | ||
orgSlug := "test-org-slug" | ||
appDir := t.TempDir() | ||
input := statusInput{appSlug: appSlug, orgSlug: orgSlug, appDir: appDir} | ||
readAppinput := app.ReadAppInput{OrganizationSlug: orgSlug, AppSlug: appSlug} | ||
readAppOutput := app.ReadAppOutput{AppID: appID} | ||
listAppWorkloadsInput := app.ListAppWorkloadsInput{AppID: appID} | ||
appWorkloads := []app.AppWorkload{ | ||
{ | ||
OrganizationSlug: "test-organization-slug", | ||
StartedAt: time.Date(2024, time.January, 1, 13, 0, 0, 0, time.UTC), | ||
Status: "RUNNING", | ||
}, | ||
{ | ||
Subscription: &app.AppWorkloadSubscription{OrganizationSlug: "test-subscribing-organization-slug", SubscriptionUUID: "test-subscription-id"}, | ||
StartedAt: time.Date(2024, time.February, 2, 14, 0, 0, 0, time.UTC), | ||
Status: "RUNNING", | ||
}, | ||
} | ||
|
||
t.Run("makes expected app read call", func(t *testing.T) { | ||
mockApps := &mockAppReaderWorkloadLister{} | ||
mockApps.On("ReadApp", mock.Anything, mock.Anything).Return(readAppOutput, nil) | ||
mockApps.On("ListAppWorkloads", mock.Anything, mock.Anything).Return(appWorkloads, nil) | ||
|
||
err := status(ctx, mockApps, input) | ||
|
||
assert.NoError(t, err) | ||
mockApps.AssertNumberOfCalls(t, "ReadApp", 1) | ||
mockApps.AssertCalled(t, "ReadApp", ctx, readAppinput) | ||
}) | ||
|
||
t.Run("makes expected app workload list call", func(t *testing.T) { | ||
mockApps := &mockAppReaderWorkloadLister{} | ||
mockApps.On("ReadApp", mock.Anything, mock.Anything).Return(readAppOutput, nil) | ||
mockApps.On("ListAppWorkloads", mock.Anything, mock.Anything).Return(appWorkloads, nil) | ||
|
||
err := status(ctx, mockApps, input) | ||
|
||
assert.NoError(t, err) | ||
mockApps.AssertNumberOfCalls(t, "ListAppWorkloads", 1) | ||
mockApps.AssertCalled(t, "ListAppWorkloads", ctx, listAppWorkloadsInput) | ||
}) | ||
|
||
t.Run("given app read error it is returned and app workload list is not called", func(t *testing.T) { | ||
mockApps := &mockAppReaderWorkloadLister{} | ||
mockApps.On("ReadApp", mock.Anything, mock.Anything).Return(app.ReadAppOutput{}, errTest) | ||
|
||
err := status(ctx, mockApps, input) | ||
|
||
assert.ErrorIs(t, err, errTest) | ||
mockApps.AssertNotCalled(t, "ListAppWorkloads") | ||
}) | ||
|
||
t.Run("given error from app workload list call it is returned", func(t *testing.T) { | ||
mockApps := &mockAppReaderWorkloadLister{} | ||
mockApps.On("ReadApp", mock.Anything, mock.Anything).Return(readAppOutput, nil) | ||
mockApps.On("ListAppWorkloads", mock.Anything, mock.Anything).Return(([]app.AppWorkload)(nil), errTest) | ||
|
||
err := status(ctx, mockApps, input) | ||
|
||
assert.ErrorIs(t, err, errTest) | ||
}) | ||
} | ||
|
||
func TestHumanDuration(t *testing.T) { | ||
type testCase struct { | ||
name string | ||
duration time.Duration | ||
expected string | ||
} | ||
|
||
for _, tc := range []testCase{ | ||
{ | ||
name: "seconds only", | ||
duration: 5 * time.Second, | ||
expected: "5 seconds", | ||
}, | ||
{ | ||
name: "seconds are rounded", | ||
duration: 7*time.Second + 10*time.Millisecond + 20*time.Microsecond, | ||
expected: "7 seconds", | ||
}, | ||
{ | ||
name: "minutes and seconds", | ||
duration: 123 * time.Second, | ||
expected: "2 minutes and 3 seconds", | ||
}, | ||
{ | ||
name: "hours and minutes", | ||
duration: 123 * time.Minute, | ||
expected: "2 hours and 3 minutes", | ||
}, | ||
{ | ||
name: "days and hours", | ||
duration: 50 * time.Hour, | ||
expected: "2 days and 2 hours", | ||
}, | ||
} { | ||
actual := humanDuration(tc.duration) | ||
assert.Equal(t, tc.expected, actual) | ||
} | ||
} |
Oops, something went wrong.