Skip to content

Commit 6219473

Browse files
authored
Move error handling for kpt live commands into kpt error resolver (#1840)
* Move error handling for kpt live commands into kpt error resolver * Addressed comments
1 parent 608ecf1 commit 6219473

File tree

5 files changed

+118
-26
lines changed

5 files changed

+118
-26
lines changed

internal/errors/resolver/resolvers.go renamed to internal/errors/resolver/git.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ Error: Unknown ref {{ printf "%q" .ref }}. Please verify that the reference exis
7373
// that can produce error messages for errors of the gitutil.GitExecError type.
7474
type gitExecErrorResolver struct{}
7575

76-
func (*gitExecErrorResolver) Resolve(err error) (string, bool) {
76+
func (*gitExecErrorResolver) Resolve(err error) (ResolvedResult, bool) {
7777
var gitExecErr *gitutil.GitExecError
7878
if !goerrors.As(err, &gitExecErr) {
79-
return "", false
79+
return ResolvedResult{}, false
8080
}
8181
fullCommand := fmt.Sprintf("git %s %s", gitExecErr.Command,
8282
strings.Join(gitExecErr.Args, " "))
@@ -87,29 +87,37 @@ func (*gitExecErrorResolver) Resolve(err error) (string, bool) {
8787
"stdout": gitExecErr.StdOut,
8888
"stderr": gitExecErr.StdErr,
8989
}
90+
var msg string
9091
switch {
9192
// TODO(mortent): Checking the content of the output at this level seems a bit awkward. We might
9293
// consider doing this the the gitutil package and use some kind of error code to signal
9394
// the different error cases to higher levels in the stack.
9495
case strings.Contains(gitExecErr.StdErr, " unknown revision or path not in the working tree"):
95-
return ExecuteTemplate(unknownRefGitExecError, tmplArgs)
96+
msg = ExecuteTemplate(unknownRefGitExecError, tmplArgs)
9697
default:
97-
return ExecuteTemplate(genericGitExecError, tmplArgs)
98+
msg = ExecuteTemplate(genericGitExecError, tmplArgs)
9899
}
100+
return ResolvedResult{
101+
Message: msg,
102+
ExitCode: 1,
103+
}, true
99104
}
100105

101106
// gitExecErrorResolver is an implementation of the ErrorResolver interface
102107
// that can produce error messages for errors of the FnExecError type.
103108
type fnExecErrorResolver struct{}
104109

105-
func (*fnExecErrorResolver) Resolve(err error) (string, bool) {
110+
func (*fnExecErrorResolver) Resolve(err error) (ResolvedResult, bool) {
106111
kioErr := errors.UnwrapKioError(err)
107112

108113
var fnErr *errors.FnExecError
109114
if !goerrors.As(kioErr, &fnErr) {
110-
return "", false
115+
return ResolvedResult{}, false
111116
}
112117
// TODO: write complete details to a file
113118

114-
return fnErr.String(), true
119+
return ResolvedResult{
120+
Message: fnErr.String(),
121+
ExitCode: 1,
122+
}, true
115123
}

internal/errors/resolver/live.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resolver
16+
17+
import (
18+
"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
19+
"sigs.k8s.io/cli-utils/pkg/inventory"
20+
)
21+
22+
//nolint:gochecknoinits
23+
func init() {
24+
AddErrorResolver(&liveErrorResolver{})
25+
}
26+
27+
const (
28+
noInventoryObjError = `
29+
Error: Package uninitialized. Please run "kpt live init" command.
30+
31+
The package needs to be initialized to generate the template
32+
which will store state for resource sets. This state is
33+
necessary to perform functionality such as deleting an entire
34+
package or automatically deleting omitted resources (pruning).
35+
`
36+
multipleInventoryObjError = `
37+
Error: Package has multiple inventory object templates.
38+
39+
The package should have one and only one inventory object template.
40+
`
41+
//nolint:lll
42+
timeoutError = `
43+
Error: Timeout after {{printf "%.0f" .err.Timeout.Seconds}} seconds waiting for {{printf "%d" (len .err.TimedOutResources)}} out of {{printf "%d" (len .err.Identifiers)}} resources to reach condition {{ .err.Condition}}:{{ printf "\n" }}
44+
45+
{{- range .err.TimedOutResources}}
46+
{{printf "%s/%s %s %s" .Identifier.GroupKind.Kind .Identifier.Name .Status .Message }}
47+
{{- end}}
48+
`
49+
50+
TimeoutErrorExitCode = 3
51+
)
52+
53+
// liveErrorResolver is an implementation of the ErrorResolver interface
54+
// that can resolve error types used in the live functionality.
55+
type liveErrorResolver struct{}
56+
57+
func (*liveErrorResolver) Resolve(err error) (ResolvedResult, bool) {
58+
tmplArgs := map[string]interface{}{
59+
"err": err,
60+
}
61+
switch err.(type) {
62+
case *inventory.NoInventoryObjError:
63+
return ResolvedResult{
64+
Message: ExecuteTemplate(noInventoryObjError, tmplArgs),
65+
}, true
66+
case *inventory.MultipleInventoryObjError:
67+
return ResolvedResult{
68+
Message: ExecuteTemplate(multipleInventoryObjError, tmplArgs),
69+
}, true
70+
case *taskrunner.TimeoutError:
71+
return ResolvedResult{
72+
Message: ExecuteTemplate(timeoutError, tmplArgs),
73+
ExitCode: TimeoutErrorExitCode,
74+
}, true
75+
default:
76+
return ResolvedResult{}, false
77+
}
78+
}

internal/errors/resolver/resolver.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ func AddErrorResolver(er ErrorResolver) {
3333
// ResolveError attempts to resolve the provided error into a descriptive
3434
// string which will be displayed to the user. If the last return value is false,
3535
// the error could not be resolved.
36-
func ResolveError(err error) (string, bool) {
36+
func ResolveError(err error) (ResolvedResult, bool) {
3737
for _, resolver := range errorResolvers {
3838
msg, found := resolver.Resolve(err)
3939
if found {
4040
return msg, true
4141
}
4242
}
43-
return "", false
43+
return ResolvedResult{}, false
4444
}
4545

4646
// ExecuteTemplate takes the provided template string and data, and renders
4747
// the template. If something goes wrong, it panics.
48-
func ExecuteTemplate(text string, data interface{}) (string, bool) {
48+
func ExecuteTemplate(text string, data interface{}) string {
4949
tmpl, tmplErr := template.New("kpterror").Parse(text)
5050
if tmplErr != nil {
5151
panic(fmt.Errorf("error creating template: %w", tmplErr))
@@ -56,11 +56,16 @@ func ExecuteTemplate(text string, data interface{}) (string, bool) {
5656
if execErr != nil {
5757
panic(fmt.Errorf("error executing template: %w", execErr))
5858
}
59-
return strings.TrimSpace(b.String()), true
59+
return strings.TrimSpace(b.String())
60+
}
61+
62+
type ResolvedResult struct {
63+
Message string
64+
ExitCode int
6065
}
6166

6267
// ErrorResolver is an interface that allows kpt to resolve an error into
6368
// an error message suitable for the end user.
6469
type ErrorResolver interface {
65-
Resolve(err error) (string, bool)
70+
Resolve(err error) (ResolvedResult, bool)
6671
}

main.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@ import (
2626

2727
"github.com/GoogleContainerTools/kpt/internal/errors"
2828
"github.com/GoogleContainerTools/kpt/internal/errors/resolver"
29-
"github.com/GoogleContainerTools/kpt/internal/util/cmdutil"
3029
"github.com/GoogleContainerTools/kpt/run"
3130
"github.com/spf13/cobra"
3231
_ "k8s.io/client-go/plugin/pkg/client/auth"
3332
"k8s.io/klog"
33+
k8scmdutil "k8s.io/kubectl/pkg/cmd/util"
3434
"k8s.io/kubectl/pkg/util/logs"
35-
cliutilserror "sigs.k8s.io/cli-utils/pkg/errors"
3635
)
3736

3837
func main() {
@@ -72,24 +71,27 @@ func runMain() int {
7271
return 0
7372
}
7473

75-
// TODO(mortent): Reconcile the different error handlers here. This is partly
76-
// a result of previously having the cobra commands in several different repos.
74+
// handleErr takes care of printing an error message for a given error.
7775
func handleErr(cmd *cobra.Command, err error) int {
78-
msg, found := resolver.ResolveError(err)
79-
if found {
80-
fmt.Fprintf(cmd.ErrOrStderr(), "\n%s \n", msg)
81-
return 1
76+
// First attempt to see if we can resolve the error into a specific
77+
// error message.
78+
re, resolved := resolver.ResolveError(err)
79+
if resolved {
80+
fmt.Fprintf(cmd.ErrOrStderr(), "\n%s \n", re.Message)
81+
return re.ExitCode
8282
}
8383

84+
// Then try to see if it is of type *errors.Error
8485
var kptErr *errors.Error
8586
if errors.As(err, &kptErr) {
8687
fmt.Fprintf(cmd.ErrOrStderr(), "%s \n", kptErr.Error())
8788
return 1
8889
}
89-
// fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", err)
90-
cmdutil.PrintErrorStacktrace(err)
91-
// TODO: find a way to avoid having to provide `kpt live` as a
92-
// parameter here.
93-
cliutilserror.CheckErr(cmd.ErrOrStderr(), err, "kpt live")
90+
91+
// Finally just let the error handler for kubectl handle it. This handles
92+
// printing of several error types used in kubectl
93+
// TODO: See if we can handle this in kpt and get a uniform experience
94+
// across all of kpt.
95+
k8scmdutil.CheckErr(err)
9496
return 1
9597
}

thirdparty/cli-utils/printers/table/printer.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ func (t *Printer) runPrintLoop(coll *ResourceStateCollector, stop chan struct{})
138138
ticker.Stop()
139139
latestState := coll.LatestState()
140140
linesPrinted = baseTablePrinter.PrintTable(latestState, linesPrinted)
141-
_, _ = fmt.Fprint(t.IOStreams.Out, "\n")
142141
return
143142
case <-ticker.C:
144143
latestState := coll.LatestState()

0 commit comments

Comments
 (0)