Skip to content

Commit df657c5

Browse files
authored
chore: add job logs skeleton (#2272)
<!-- Provide summary of changes --> This PR combines a small refactor to separate out common fields from `svc logs` for reuse, and the addition of the skeleton for `job logs`. <!-- Issue number, if available. E.g. "Fixes #31", "Addresses #42, 77" --> By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 6cf8182 commit df657c5

File tree

7 files changed

+394
-37
lines changed

7 files changed

+394
-37
lines changed

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
12181218
golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
12191219
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
12201220
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
1221+
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
12211222
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
12221223
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
12231224
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

internal/pkg/cli/flag.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ const (
103103
valuesFlag = "values"
104104
overwriteFlag = "overwrite"
105105
inputFilePathFlag = "cli-input-yaml"
106+
107+
includeStateMachineLogsFlag = "include-state-machine"
106108
)
107109

108110
// Short flag names.
@@ -193,8 +195,9 @@ Defaults to all logs. Only one of start-time / since may be used.`
193195
Defaults to all logs. Only one of start-time / since may be used.`
194196
endTimeFlagDescription = `Optional. Only return logs before a specific date (RFC3339).
195197
Defaults to all logs. Only one of end-time / follow may be used.`
196-
tasksLogsFlagDescription = "Optional. Only return logs from specific task IDs."
197-
logGroupFlagDescription = "Optional. Only return logs from specific log group."
198+
tasksLogsFlagDescription = "Optional. Only return logs from specific task IDs."
199+
includeStateMachineLogsFlagDescription = "Optional. Include logs from the state machine executions."
200+
logGroupFlagDescription = "Optional. Only return logs from specific log group."
198201

199202
deployTestFlagDescription = `Deploy your service or job to a "test" environment.`
200203
githubURLFlagDescription = "(Deprecated.) Use --url instead. Repository URL to trigger your pipeline."

internal/pkg/cli/job.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Jobs are tasks that are triggered by events.`,
2424
cmd.AddCommand(buildJobPackageCmd())
2525
cmd.AddCommand(buildJobDeployCmd())
2626
cmd.AddCommand(buildJobDeleteCmd())
27+
cmd.AddCommand(buildJobLogsCmd())
2728

2829
cmd.SetUsageTemplate(template.Usage)
2930

internal/pkg/cli/job_logs.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cli
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
10+
"github.com/aws/aws-sdk-go/aws"
11+
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
12+
"github.com/aws/copilot-cli/internal/pkg/config"
13+
"github.com/aws/copilot-cli/internal/pkg/deploy"
14+
"github.com/aws/copilot-cli/internal/pkg/logging"
15+
"github.com/aws/copilot-cli/internal/pkg/term/log"
16+
"github.com/aws/copilot-cli/internal/pkg/term/prompt"
17+
"github.com/aws/copilot-cli/internal/pkg/term/selector"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
const (
22+
jobAppNamePrompt = "Which application does your job belong to?"
23+
)
24+
25+
type jobLogsVars struct {
26+
wkldLogsVars
27+
28+
includeStateMachineLogs bool // Whether to include the logs from the state machine log streams
29+
}
30+
31+
type jobLogsOpts struct {
32+
jobLogsVars
33+
34+
wkldLogOpts
35+
}
36+
37+
func newJobLogOpts(vars jobLogsVars) (*jobLogsOpts, error) {
38+
configStore, err := config.NewStore()
39+
if err != nil {
40+
return nil, fmt.Errorf("connect to environment config store: %w", err)
41+
}
42+
deployStore, err := deploy.NewStore(configStore)
43+
if err != nil {
44+
return nil, fmt.Errorf("connect to deploy store: %w", err)
45+
}
46+
opts := &jobLogsOpts{
47+
jobLogsVars: vars,
48+
wkldLogOpts: wkldLogOpts{
49+
w: log.OutputWriter,
50+
configStore: configStore,
51+
deployStore: deployStore,
52+
sel: selector.NewDeploySelect(prompt.New(), configStore, deployStore),
53+
},
54+
}
55+
opts.initLogsSvc = func() error {
56+
env, err := opts.configStore.GetEnvironment(opts.appName, opts.envName)
57+
if err != nil {
58+
return fmt.Errorf("get environment: %w", err)
59+
}
60+
sess, err := sessions.NewProvider().FromRole(env.ManagerRoleARN, env.Region)
61+
if err != nil {
62+
return err
63+
}
64+
opts.logsSvc, err = logging.NewServiceClient(&logging.NewServiceLogsConfig{
65+
Sess: sess,
66+
App: opts.appName,
67+
Env: opts.envName,
68+
Svc: opts.name,
69+
})
70+
if err != nil {
71+
return err
72+
}
73+
return nil
74+
}
75+
return opts, nil
76+
}
77+
78+
// Validate returns an error if the values provided by flags are invalid.
79+
func (o *jobLogsOpts) Validate() error {
80+
if o.appName != "" {
81+
_, err := o.configStore.GetApplication(o.appName)
82+
if err != nil {
83+
return err
84+
}
85+
}
86+
87+
if o.since != 0 && o.humanStartTime != "" {
88+
return errors.New("only one of --since or --start-time may be used")
89+
}
90+
91+
if o.humanEndTime != "" && o.follow {
92+
return errors.New("only one of --follow or --end-time may be used")
93+
}
94+
95+
if o.since != 0 {
96+
if o.since < 0 {
97+
return fmt.Errorf("--since must be greater than 0")
98+
}
99+
// round up to the nearest second
100+
o.startTime = parseSince(o.since)
101+
}
102+
103+
if o.humanStartTime != "" {
104+
startTime, err := parseRFC3339(o.humanStartTime)
105+
if err != nil {
106+
return fmt.Errorf(`invalid argument %s for "--start-time" flag: %w`, o.humanStartTime, err)
107+
}
108+
o.startTime = aws.Int64(startTime)
109+
}
110+
111+
if o.humanEndTime != "" {
112+
endTime, err := parseRFC3339(o.humanEndTime)
113+
if err != nil {
114+
return fmt.Errorf(`invalid argument %s for "--end-time" flag: %w`, o.humanEndTime, err)
115+
}
116+
o.endTime = aws.Int64(endTime)
117+
}
118+
119+
if o.limit != 0 && (o.limit < cwGetLogEventsLimitMin || o.limit > cwGetLogEventsLimitMax) {
120+
return fmt.Errorf("--limit %d is out-of-bounds, value must be between %d and %d", o.limit, cwGetLogEventsLimitMin, cwGetLogEventsLimitMax)
121+
}
122+
123+
return nil
124+
}
125+
126+
// Ask asks for fields that are required but not passed in.
127+
func (o *jobLogsOpts) Ask() error {
128+
if err := o.askApp(); err != nil {
129+
return err
130+
}
131+
return nil
132+
}
133+
134+
func (o *jobLogsOpts) askApp() error {
135+
if o.appName != "" {
136+
return nil
137+
}
138+
app, err := o.sel.Application(jobAppNamePrompt, svcAppNameHelpPrompt)
139+
if err != nil {
140+
return fmt.Errorf("select application: %w", err)
141+
}
142+
o.appName = app
143+
return nil
144+
}
145+
146+
// Execute outputs logs of the job.
147+
func (o *jobLogsOpts) Execute() error {
148+
return nil
149+
}
150+
151+
// buildJobLogsCmd builds the command for displaying job logs in an application.
152+
func buildJobLogsCmd() *cobra.Command {
153+
vars := jobLogsVars{}
154+
cmd := &cobra.Command{
155+
Use: "logs",
156+
Short: "Displays logs of a deployed job.",
157+
Hidden: true,
158+
Example: `
159+
Displays logs of the job "my-job" in environment "test".
160+
/code $ copilot job logs -n my-job -e test
161+
Displays logs in the last hour.
162+
/code $ copilot job logs --since 1h
163+
Displays logs from 2006-01-02T15:04:05 to 2006-01-02T15:05:05.
164+
/code $ copilot job logs --start-time 2006-01-02T15:04:05+00:00 --end-time 2006-01-02T15:05:05+00:00
165+
Displays logs from specific task IDs.
166+
/code $ copilot job logs --tasks 709c7eae05f947f6861b150372ddc443,1de57fd63c6a4920ac416d02add891b9
167+
Displays logs in real time.
168+
/code $ copilot job logs --follow
169+
Displays container logs and state machine execution logs from the last execution.
170+
/code $ copilot job logs --include-state-machine`,
171+
RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
172+
opts, err := newJobLogOpts(vars)
173+
if err != nil {
174+
return err
175+
}
176+
if err := opts.Validate(); err != nil {
177+
return err
178+
}
179+
if err := opts.Ask(); err != nil {
180+
return err
181+
}
182+
return opts.Execute()
183+
}),
184+
}
185+
cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", svcFlagDescription)
186+
cmd.Flags().StringVarP(&vars.envName, envFlag, envFlagShort, "", envFlagDescription)
187+
cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription)
188+
cmd.Flags().StringVar(&vars.humanStartTime, startTimeFlag, "", startTimeFlagDescription)
189+
cmd.Flags().StringVar(&vars.humanEndTime, endTimeFlag, "", endTimeFlagDescription)
190+
cmd.Flags().BoolVar(&vars.shouldOutputJSON, jsonFlag, false, jsonFlagDescription)
191+
cmd.Flags().BoolVar(&vars.follow, followFlag, false, followFlagDescription)
192+
cmd.Flags().DurationVar(&vars.since, sinceFlag, 0, sinceFlagDescription)
193+
cmd.Flags().IntVar(&vars.limit, limitFlag, 0, limitFlagDescription)
194+
cmd.Flags().StringSliceVar(&vars.taskIDs, tasksFlag, nil, tasksLogsFlagDescription)
195+
cmd.Flags().BoolVar(&vars.includeStateMachineLogs, includeStateMachineLogsFlag, false, includeStateMachineLogsFlagDescription)
196+
return cmd
197+
}

internal/pkg/cli/job_logs_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cli
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/aws/copilot-cli/internal/pkg/cli/mocks"
13+
14+
"github.com/golang/mock/gomock"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestJobLogs_Validate(t *testing.T) {
19+
const (
20+
mockSince = 1 * time.Minute
21+
mockStartTime = "1970-01-01T01:01:01+00:00"
22+
mockBadStartTime = "badStartTime"
23+
mockEndTime = "1971-01-01T01:01:01+00:00"
24+
mockBadEndTime = "badEndTime"
25+
)
26+
testCases := map[string]struct {
27+
inputApp string
28+
inputSvc string
29+
inputLimit int
30+
inputFollow bool
31+
inputEnvName string
32+
inputStartTime string
33+
inputEndTime string
34+
inputSince time.Duration
35+
36+
mockstore func(m *mocks.Mockstore)
37+
38+
wantedError error
39+
}{
40+
"with no flag set": {
41+
mockstore: func(m *mocks.Mockstore) {},
42+
43+
wantedError: nil,
44+
},
45+
"invalid project name": {
46+
inputApp: "my-app",
47+
48+
mockstore: func(m *mocks.Mockstore) {
49+
m.EXPECT().GetApplication("my-app").Return(nil, errors.New("some error"))
50+
},
51+
52+
wantedError: fmt.Errorf("some error"),
53+
},
54+
"returns error if since and startTime flags are set together": {
55+
inputSince: mockSince,
56+
inputStartTime: mockStartTime,
57+
58+
mockstore: func(m *mocks.Mockstore) {},
59+
60+
wantedError: fmt.Errorf("only one of --since or --start-time may be used"),
61+
},
62+
"returns error if follow and endTime flags are set together": {
63+
inputFollow: true,
64+
inputEndTime: mockEndTime,
65+
66+
mockstore: func(m *mocks.Mockstore) {},
67+
68+
wantedError: fmt.Errorf("only one of --follow or --end-time may be used"),
69+
},
70+
"returns error if invalid start time flag value": {
71+
inputStartTime: mockBadStartTime,
72+
73+
mockstore: func(m *mocks.Mockstore) {},
74+
75+
wantedError: fmt.Errorf("invalid argument badStartTime for \"--start-time\" flag: reading time value badStartTime: parsing time \"badStartTime\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"badStartTime\" as \"2006\""),
76+
},
77+
"returns error if invalid end time flag value": {
78+
inputEndTime: mockBadEndTime,
79+
80+
mockstore: func(m *mocks.Mockstore) {},
81+
82+
wantedError: fmt.Errorf("invalid argument badEndTime for \"--end-time\" flag: reading time value badEndTime: parsing time \"badEndTime\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"badEndTime\" as \"2006\""),
83+
},
84+
"returns error if invalid since flag value": {
85+
inputSince: -mockSince,
86+
87+
mockstore: func(m *mocks.Mockstore) {},
88+
89+
wantedError: fmt.Errorf("--since must be greater than 0"),
90+
},
91+
"returns error if limit value is below limit": {
92+
inputLimit: -1,
93+
94+
mockstore: func(m *mocks.Mockstore) {},
95+
96+
wantedError: fmt.Errorf("--limit -1 is out-of-bounds, value must be between 1 and 10000"),
97+
},
98+
"returns error if limit value is above limit": {
99+
inputLimit: 10001,
100+
101+
mockstore: func(m *mocks.Mockstore) {},
102+
103+
wantedError: fmt.Errorf("--limit 10001 is out-of-bounds, value must be between 1 and 10000"),
104+
},
105+
}
106+
107+
for name, tc := range testCases {
108+
t.Run(name, func(t *testing.T) {
109+
ctrl := gomock.NewController(t)
110+
defer ctrl.Finish()
111+
112+
mockstore := mocks.NewMockstore(ctrl)
113+
tc.mockstore(mockstore)
114+
115+
jobLogs := &jobLogsOpts{
116+
jobLogsVars: jobLogsVars{
117+
wkldLogsVars: wkldLogsVars{
118+
follow: tc.inputFollow,
119+
limit: tc.inputLimit,
120+
envName: tc.inputEnvName,
121+
humanStartTime: tc.inputStartTime,
122+
humanEndTime: tc.inputEndTime,
123+
since: tc.inputSince,
124+
name: tc.inputSvc,
125+
appName: tc.inputApp,
126+
},
127+
},
128+
wkldLogOpts: wkldLogOpts{
129+
configStore: mockstore,
130+
},
131+
}
132+
133+
// WHEN
134+
err := jobLogs.Validate()
135+
136+
// THEN
137+
if tc.wantedError != nil {
138+
require.EqualError(t, err, tc.wantedError.Error())
139+
} else {
140+
require.NoError(t, err)
141+
}
142+
})
143+
}
144+
}

0 commit comments

Comments
 (0)