From 50c0a3112c89f30c44ac1d91560eeffe527d9ed3 Mon Sep 17 00:00:00 2001 From: Mederic Bazart Date: Thu, 9 Mar 2023 17:59:41 +0100 Subject: [PATCH 1/5] feat(one-off): allow attached one-off to be run asynchronously --- apps/operations.go | 43 +++++++++++++++++++++++++++++++++++++++---- apps/run.go | 30 +++++++++++++++++++++++++----- cmd/run.go | 1 + 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/apps/operations.go b/apps/operations.go index 644ceaf49..ea3b5cf36 100644 --- a/apps/operations.go +++ b/apps/operations.go @@ -17,14 +17,19 @@ import ( ) func handleOperation(ctx context.Context, app string, res *http.Response) error { - opURL, err := url.Parse(res.Header.Get("Location")) + operationURL := res.Header.Get("Location") + return handleOperationWithURL(ctx, app, operationURL) +} + +func handleOperationWithURL(ctx context.Context, app string, operationURL string, containerLabel ...string) error { + opURL, err := url.Parse(operationURL) if err != nil { return errgo.Mask(err) } c, err := config.ScalingoClient(ctx) if err != nil { - return errgo.Notef(err, "fail to get Scalingo client") + return errgo.Notef(err, "get Scalingo client") } var op *scalingo.Operation @@ -35,6 +40,11 @@ func handleOperation(ctx context.Context, app string, res *http.Response) error defer close(done) defer close(errs) + op, err = c.OperationsShow(ctx, app, opID) + if err != nil { + return errgo.Notef(err, "get operation") + } + go func() { for { op, err = c.OperationsShow(ctx, app, opID) @@ -51,7 +61,11 @@ func handleOperation(ctx context.Context, app string, res *http.Response) error } }() - fmt.Print("Status: ") + if op.Type == scalingo.OperationTypeStartOneOff { + fmt.Printf("-----> Starting container %v ", containerLabel) + } else { + fmt.Print("Status: ") + } spinner := io.NewSpinner(os.Stderr) go spinner.Start() defer spinner.Stop() @@ -66,8 +80,29 @@ func handleOperation(ctx context.Context, app string, res *http.Response) error return nil } else if op.Status == "error" { fmt.Printf("\bOperation '%s' failed, an error occurred: %v\n", op.Type, op.Error) - return nil + return errgo.Newf("operation %v failed", op.ID) } } } } + +func GetAttachURLFromOperationWithURL(ctx context.Context, app string, operationURL string) (string, error) { + opURL, err := url.Parse(operationURL) + if err != nil { + return "", errgo.Mask(err) + } + + c, err := config.ScalingoClient(ctx) + if err != nil { + return "", errgo.Notef(err, "get Scalingo client") + } + + var operation *scalingo.Operation + opID := filepath.Base(opURL.Path) + operation, err = c.OperationsShow(ctx, app, opID) + if err != nil { + return "", errgo.Notef(err, "get operation") + } + + return operation.OneOffData.AttachURL, nil +} diff --git a/apps/run.go b/apps/run.go index 1c27b5acd..95f5c957e 100644 --- a/apps/run.go +++ b/apps/run.go @@ -37,6 +37,7 @@ type RunOpts struct { DisplayCmd string Silent bool Detached bool + Async bool Size string Type string Cmd []string @@ -58,7 +59,7 @@ type runContext struct { func Run(ctx context.Context, opts RunOpts) error { c, err := config.ScalingoClient(ctx) if err != nil { - return errgo.Notef(err, "fail to get Scalingo client") + return errgo.Notef(err, "get Scalingo client") } firstReadDone := make(chan struct{}) @@ -103,6 +104,9 @@ func Run(ctx context.Context, opts RunOpts) error { if opts.StdoutCopyFunc != nil { runCtx.stdoutCopyFunc = opts.StdoutCopyFunc } + if opts.Detached { + opts.Async = false + } env, err := runCtx.buildEnv(opts.CmdEnv) if err != nil { @@ -122,10 +126,13 @@ func Run(ctx context.Context, opts RunOpts) error { Env: env, Size: opts.Size, Detached: opts.Detached, - }) + Async: opts.Async, + }, + ) if err != nil { return errgo.Mask(err, errgo.Any) } + debug.Printf("%+v\n", runRes) if opts.Detached { @@ -138,7 +145,17 @@ func Run(ctx context.Context, opts RunOpts) error { return nil } - runCtx.attachURL = runRes.AttachURL + err = handleOperationWithURL(ctx, opts.App, runRes.OperationURL, runRes.Container.Label) + if err != nil { + return errgo.Mask(err, errgo.Any) + } + + attachURL, err := GetAttachURLFromOperationWithURL(ctx, opts.App, runRes.OperationURL) + if err != nil { + return errgo.Mask(err, errgo.Any) + } + + runCtx.attachURL = attachURL debug.Println("Run Service URL is", runCtx.attachURL) if len(opts.Files) > 0 { @@ -221,6 +238,9 @@ func Run(ctx context.Context, opts RunOpts) error { }() _, err = runCtx.stdoutCopyFunc(os.Stdout, socket) + if err != nil { + return errgo.Mask(err, errgo.Any) + } stopSignalsMonitoring <- true @@ -235,11 +255,11 @@ func Run(ctx context.Context, opts RunOpts) error { return errgo.Mask(err, errgo.Any) } - os.Exit(exitCode) + defer os.Exit(exitCode) return nil } -func (ctx *runContext) buildEnv(cmdEnv []string) (map[string]string, error) { +func (runCtx *runContext) buildEnv(cmdEnv []string) (map[string]string, error) { env := map[string]string{ "TERM": os.Getenv("TERM"), "CLIENT_OS": runtime.GOOS, diff --git a/cmd/run.go b/cmd/run.go index 348aaa750..43fa5a370 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -87,6 +87,7 @@ var ( Files: c.StringSlice("f"), Silent: c.Bool("silent"), Detached: c.Bool("detached"), + Async: true, } if (c.Args().Len() == 0 && c.String("t") == "") || (c.Args().Len() > 0 && c.String("t") != "") { cli.ShowCommandHelp(c, "run") From 111c01f3c05e2739d2e337930f66a15b93f53a4a Mon Sep 17 00:00:00 2001 From: Mederic Bazart Date: Wed, 15 Mar 2023 18:04:44 +0100 Subject: [PATCH 2/5] fix(one-off) update operation due to changes on go-scalingo --- apps/operations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/operations.go b/apps/operations.go index ea3b5cf36..b97a686a2 100644 --- a/apps/operations.go +++ b/apps/operations.go @@ -104,5 +104,5 @@ func GetAttachURLFromOperationWithURL(ctx context.Context, app string, operation return "", errgo.Notef(err, "get operation") } - return operation.OneOffData.AttachURL, nil + return operation.StartOneOffData.AttachURL, nil } From 0b193d1382117d88eb6dd99ee97ab008e4a56a74 Mon Sep 17 00:00:00 2001 From: Mederic Bazart Date: Thu, 16 Mar 2023 14:55:53 +0100 Subject: [PATCH 3/5] chore(deps): update go-scalingo to v6.4.0 --- go.mod | 3 +- go.sum | 6 +- .../Scalingo/go-scalingo/v6/CHANGELOG.md | 6 ++ .../Scalingo/go-scalingo/v6/Dockerfile | 4 +- .../Scalingo/go-scalingo/v6/README.md | 4 +- .../Scalingo/go-scalingo/v6/backups.go | 10 +- .../Scalingo/go-scalingo/v6/deployments.go | 1 + .../Scalingo/go-scalingo/v6/operations.go | 25 +++-- .../github.com/Scalingo/go-scalingo/v6/run.go | 16 ++-- .../Scalingo/go-scalingo/v6/version.go | 2 +- .../Scalingo/go-utils/errors/v2/CHANGELOG.md | 31 ++++++ .../Scalingo/go-utils/errors/v2/LICENSE | 19 ++++ .../Scalingo/go-utils/errors/v2/README.md | 3 + .../Scalingo/go-utils/errors/v2/cause.go | 68 +++++++++++++ .../Scalingo/go-utils/errors/v2/errctx.go | 95 ++++++++++++++++++ .../Scalingo/go-utils/errors/v2/errgo.go | 22 +++++ .../Scalingo/go-utils/errors/v2/errors.go | 21 ++++ .../go-utils/errors/v2/validation_errors.go | 96 +++++++++++++++++++ vendor/modules.txt | 7 +- 19 files changed, 413 insertions(+), 26 deletions(-) create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/CHANGELOG.md create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/LICENSE create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/README.md create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/cause.go create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/errctx.go create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/errgo.go create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/errors.go create mode 100644 vendor/github.com/Scalingo/go-utils/errors/v2/validation_errors.go diff --git a/go.mod b/go.mod index 73e12f113..482282dfd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/AlecAivazis/survey/v2 v2.3.6 github.com/ScaleFT/sshkeys v1.2.0 - github.com/Scalingo/go-scalingo/v6 v6.3.0 + github.com/Scalingo/go-scalingo/v6 v6.4.0 github.com/Scalingo/go-utils/errors v1.1.1 github.com/Scalingo/go-utils/logger v1.2.0 github.com/Scalingo/go-utils/retry v1.1.1 @@ -33,6 +33,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.0 // indirect github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect + github.com/Scalingo/go-utils/errors/v2 v2.2.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/cloudflare/circl v1.3.2 // indirect diff --git a/go.sum b/go.sum index e445552d0..8f03c63d2 100644 --- a/go.sum +++ b/go.sum @@ -9,10 +9,12 @@ github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0A github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/ScaleFT/sshkeys v1.2.0 h1:5BRp6rTVIhJzXT3VcUQrKgXR8zWA3sOsNeuyW15WUA8= github.com/ScaleFT/sshkeys v1.2.0/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o= -github.com/Scalingo/go-scalingo/v6 v6.3.0 h1:ADtzCCEqx1pdI6VaiOh3u9l8L2tScPp6cIKzvo/lcgE= -github.com/Scalingo/go-scalingo/v6 v6.3.0/go.mod h1:Y3QByF4s5i3lpJh87c5FEkSN07CWomN5FPt0X/Qzg0I= +github.com/Scalingo/go-scalingo/v6 v6.4.0 h1:kkSJlPE0SavvO7yrH8dpdYIw2te8d0th5NzkAISS/BY= +github.com/Scalingo/go-scalingo/v6 v6.4.0/go.mod h1:u3ROWLWw78ug4EQaunzOBzSPKy4ukNQTDrCFaKMG938= github.com/Scalingo/go-utils/errors v1.1.1 h1:G7zypaDBj0O6QFvX0xL5dMDXbiBgq+3KKuodldtOpg0= github.com/Scalingo/go-utils/errors v1.1.1/go.mod h1:gsnGOMma5x3CSgPb8i5eMuHcKi3RvTJ9dWPNRev161c= +github.com/Scalingo/go-utils/errors/v2 v2.2.0 h1:n93hge0DzfZ3KbI/jdnxKDTRDD+PXsGwNPKyHRzQYEE= +github.com/Scalingo/go-utils/errors/v2 v2.2.0/go.mod h1:pkLy6Qz9UNm6FpXtFJGZRC0W5lqbqHpPchrQV80gw5E= github.com/Scalingo/go-utils/logger v1.2.0 h1:E3jtaoRxpIsFcZu/jsvWew8ttUAwKUYQufdPqGYp7EU= github.com/Scalingo/go-utils/logger v1.2.0/go.mod h1:JArjD1gHdB/vwnlcVG7rYxuIY0tk8/VG4MtirnRwn8k= github.com/Scalingo/go-utils/retry v1.1.1 h1:zc5HbXbBzf0fo7zMvhEMvx75qaL2FH7SxDhCoBfS7jE= diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/CHANGELOG.md b/vendor/github.com/Scalingo/go-scalingo/v6/CHANGELOG.md index e6ba049a5..3cd5de56d 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/CHANGELOG.md +++ b/vendor/github.com/Scalingo/go-scalingo/v6/CHANGELOG.md @@ -2,6 +2,12 @@ ## To Be Released +## 6.4.0 + +* feat(one-off): allow attached one-off to be run asynchronously +* feat(stacks): add support for Deployment `stack_base_image` +* feat(backup): add `StartedAt` and `Method` fields + ## 6.3.0 * feat(apps): add support for `hds_resource` diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/Dockerfile b/vendor/github.com/Scalingo/go-scalingo/v6/Dockerfile index 9217acdae..f2592ba69 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/Dockerfile +++ b/vendor/github.com/Scalingo/go-scalingo/v6/Dockerfile @@ -1,7 +1,7 @@ -FROM golang:1.17 +FROM golang:1.20 MAINTAINER Étienne Michon "etienne@scalingo.com" -RUN go get github.com/cespare/reflex +RUN go install github.com/cespare/reflex@latest WORKDIR $GOPATH/src/github.com/Scalingo/go-scalingo diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/README.md b/vendor/github.com/Scalingo/go-scalingo/v6/README.md index 2f97937e0..fa4b4b105 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/README.md +++ b/vendor/github.com/Scalingo/go-scalingo/v6/README.md @@ -1,6 +1,6 @@ [ ![Codeship Status for Scalingo/go-scalingo](https://app.codeship.com/projects/cf518dc0-0034-0136-d6b3-5a0245e77f67/status?branch=master)](https://app.codeship.com/projects/279805) -# Go client for Scalingo API v6.3.0 +# Go client for Scalingo API v6.4.0 This repository is the Go client for the [Scalingo APIs](https://developers.scalingo.com/). @@ -80,7 +80,7 @@ Bump new version number in: Commit, tag and create a new release: ```sh -version="6.3.0" +version="6.4.0" git switch --create release/${version} git add CHANGELOG.md README.md version.go diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/backups.go b/vendor/github.com/Scalingo/go-scalingo/v6/backups.go index e5ac6a4bb..766205de5 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/backups.go +++ b/vendor/github.com/Scalingo/go-scalingo/v6/backups.go @@ -26,14 +26,22 @@ const ( BackupStatusError BackupStatus = "error" ) +type BackupMethod string + +const ( + BackupMethodPeriodic BackupMethod = "periodic" + BackupMethodManual BackupMethod = "manual" +) + type Backup struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` + StartedAt time.Time `json:"started_at"` Name string `json:"name"` Size uint64 `json:"size"` Status BackupStatus `json:"status"` DatabaseID string `json:"database_id"` - Direct bool `json:"direct"` + Method BackupMethod `json:"method"` } type BackupsRes struct { diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/deployments.go b/vendor/github.com/Scalingo/go-scalingo/v6/deployments.go index d42ae24e4..9a7a5e32f 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/deployments.go +++ b/vendor/github.com/Scalingo/go-scalingo/v6/deployments.go @@ -50,6 +50,7 @@ type Deployment struct { Duration int `json:"duration"` PostdeployHook string `json:"postdeploy_hook"` ImageSize uint64 `json:"image_size"` + StackBaseImage string `json:"stack_base_image"` User *User `json:"pusher"` Links *DeploymentLinks `json:"links"` } diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/operations.go b/vendor/github.com/Scalingo/go-scalingo/v6/operations.go index bbf58fb39..e9141f89f 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/operations.go +++ b/vendor/github.com/Scalingo/go-scalingo/v6/operations.go @@ -27,22 +27,29 @@ const ( type OperationType string const ( - OperationTypeScale OperationType = "scale" - OperationTypeStart OperationType = "start" + OperationTypeScale OperationType = "scale" + OperationTypeStart OperationType = "start" + OperationTypeStartOneOff OperationType = "start-one-off" ) type OperationResponse struct { Op Operation `json:"operation"` } +type OperationStartOneOffData struct { + AttachURL string `json:"attach_url"` + ContainerID string `json:"container_id"` +} + type Operation struct { - ID string `json:"id"` - AppID string `json:"app_id"` - CreatedAt time.Time `json:"created_at"` - FinishedAt time.Time `json:"finished_at"` - Status OperationStatus `json:"status"` - Type OperationType `json:"type"` - Error string `json:"error"` + ID string `json:"id"` + AppID string `json:"app_id"` + CreatedAt time.Time `json:"created_at"` + FinishedAt time.Time `json:"finished_at"` + Status OperationStatus `json:"status"` + Type OperationType `json:"type"` + Error string `json:"error"` + StartOneOffData OperationStartOneOffData `json:"start_one_off_data"` } func (op *Operation) ElapsedDuration() float64 { diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/run.go b/vendor/github.com/Scalingo/go-scalingo/v6/run.go index dbfca90ee..59549c9ef 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/run.go +++ b/vendor/github.com/Scalingo/go-scalingo/v6/run.go @@ -5,9 +5,8 @@ import ( "encoding/json" "strings" - "gopkg.in/errgo.v1" - "github.com/Scalingo/go-scalingo/v6/http" + errors "github.com/Scalingo/go-utils/errors/v2" ) type RunsService interface { @@ -22,12 +21,14 @@ type RunOpts struct { Env map[string]string Size string Detached bool + Async bool HasUploads bool } type RunRes struct { - Container *Container `json:"container"` - AttachURL string `json:"attach_url"` + Container *Container `json:"container"` + AttachURL string `json:"attach_url"` + OperationURL string `json:"-"` } func (c *Client) Run(ctx context.Context, opts RunOpts) (*RunRes, error) { @@ -39,20 +40,23 @@ func (c *Client) Run(ctx context.Context, opts RunOpts) (*RunRes, error) { "env": opts.Env, "size": opts.Size, "detached": opts.Detached, + "async": opts.Async, "has_uploads": opts.HasUploads, }, } res, err := c.ScalingoAPI().Do(ctx, req) if err != nil { - return nil, errgo.Mask(err) + return nil, errors.Notef(ctx, err, "request endpoint %v", req.Endpoint) } defer res.Body.Close() var runRes RunRes err = json.NewDecoder(res.Body).Decode(&runRes) if err != nil { - return nil, errgo.Mask(err) + return nil, errors.Notef(ctx, err, "decode response body") } + runRes.OperationURL = res.Header.Get("Location") + return &runRes, nil } diff --git a/vendor/github.com/Scalingo/go-scalingo/v6/version.go b/vendor/github.com/Scalingo/go-scalingo/v6/version.go index 2a38fd450..71e062f19 100644 --- a/vendor/github.com/Scalingo/go-scalingo/v6/version.go +++ b/vendor/github.com/Scalingo/go-scalingo/v6/version.go @@ -1,3 +1,3 @@ package scalingo -var Version = "6.3.0" +var Version = "6.4.0" diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/CHANGELOG.md b/vendor/github.com/Scalingo/go-utils/errors/v2/CHANGELOG.md new file mode 100644 index 000000000..2dfb9b6f4 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## To be Released + +## v2.2.0 + +* feat: add UnwrapError to unwrap one error. + +## v2.1.0 + +* feat: add New function to `ErrCtx` +* feat: IsRootCause and RootCause are taking in account `ErrCtx` underlying errors +* feat: RootCtxOrFallback retrieves the deepest context from wrapped errors. + +## v2.0.0 + +* fix: privatify `ErrgoRoot` +* build(deps): bump github.com/stretchr/testify from 1.8.0 to 1.8.1 + +## v1.1.1 + +* chore(go): use go 1.17 +* build(deps): bump github.com/stretchr/testify from 1.7.0 to 1.7.1 + +## v1.1.0 + +* Bump go version to 1.16 + +## v1.0.0 + +* Initial breakdown of go-utils into subpackages diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/LICENSE b/vendor/github.com/Scalingo/go-utils/errors/v2/LICENSE new file mode 100644 index 000000000..8e1c1aa79 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Scalingo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/README.md b/vendor/github.com/Scalingo/go-utils/errors/v2/README.md new file mode 100644 index 000000000..a9f737177 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/README.md @@ -0,0 +1,3 @@ +# Package `errors` v2.2.0 + +The package `errors` contains various utility regarding errors management. diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/cause.go b/vendor/github.com/Scalingo/go-utils/errors/v2/cause.go new file mode 100644 index 000000000..9d6f2c723 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/cause.go @@ -0,0 +1,68 @@ +package errors + +import ( + "reflect" + + "gopkg.in/errgo.v1" +) + +// IsRootCause return true if the cause of the given error is the same type as +// mytype. +// This function takes the cause of an error if the errors stack has been +// wrapped with errors.Wrapf or errgo.Notef or errgo.NoteMask or errgo.Mask. +// +// Example: +// +// errors.IsRootCause(err, &ValidationErrors{}) +func IsRootCause(err error, mytype interface{}) bool { + t := reflect.TypeOf(mytype) + errCause := errorCause(err) + errRoot := errgoRoot(err) + return reflect.TypeOf(errCause) == t || reflect.TypeOf(errRoot) == t +} + +// RootCause returns the cause of an errors stack, whatever the method they used +// to be stacked: either errgo.Notef or errors.Wrapf. +func RootCause(err error) error { + errCause := errorCause(err) + if errCause == nil { + errCause = errgoRoot(err) + } + return errCause +} + +// UnwrapError tries to unwrap `err`. It unwraps any causer type, errgo and ErrCtx errors. +// It returns nil if no err found. This provide the possibility to loop on UnwrapError +// by checking the return value. +// E.g.: +// +// for unwrappedErr := err; unwrappedErr != nil; unwrappedErr = UnwrapError(unwrappedErr) { +// ... +// } +func UnwrapError(err error) error { + if err == nil { + return nil + } + + type causer interface { + Cause() error + } + + // if err is type of `ErrCtx` unwrap it by getting errCtx.err + if ctxerr, ok := err.(ErrCtx); ok { + return ctxerr.err + } + + // Check if the err is type of `*errgo.Err` to be able to call `Underlying()` + // method. Both `*errgo.Err` and `*errors.Err` are implementing a causer interface. + // Cause() method from errgo skip all underlying errors, so we may skip a context between. + // So the order matter, we need to call `Cause()` after `Underlying()`. + if errgoErr, ok := err.(*errgo.Err); ok { + return errgoErr.Underlying() + } + + if cause, ok := err.(causer); ok { + return cause.Cause() + } + return nil +} diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/errctx.go b/vendor/github.com/Scalingo/go-utils/errors/v2/errctx.go new file mode 100644 index 000000000..44cd5e9fe --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/errctx.go @@ -0,0 +1,95 @@ +package errors + +import ( + "context" + + "github.com/pkg/errors" + "gopkg.in/errgo.v1" +) + +type ErrCtx struct { + ctx context.Context + err error +} + +func (err ErrCtx) Error() string { + return err.err.Error() +} + +func (err ErrCtx) Ctx() context.Context { + return err.ctx +} + +func New(ctx context.Context, message string) error { + return ErrCtx{ctx: ctx, err: errgo.New(message)} +} + +func Newf(ctx context.Context, format string, args ...interface{}) error { + return ErrCtx{ctx: ctx, err: errgo.Newf(format, args...)} +} + +func NoteMask(ctx context.Context, err error, message string) error { + return ErrCtx{ctx: ctx, err: errgo.NoteMask(err, message)} +} + +func Notef(ctx context.Context, err error, format string, args ...interface{}) error { + return ErrCtx{ctx: ctx, err: errgo.Notef(err, format, args...)} +} + +func Wrap(ctx context.Context, err error, message string) error { + return ErrCtx{ctx: ctx, err: errors.Wrap(err, message)} +} + +func Wrapf(ctx context.Context, err error, format string, args ...interface{}) error { + return ErrCtx{ctx: ctx, err: errors.Wrapf(err, format, args...)} +} + +func Errorf(ctx context.Context, format string, args ...interface{}) error { + return ErrCtx{ctx: ctx, err: errors.Errorf(format, args...)} +} + +// RootCtxOrFallback unwrap all wrapped errors from err to get the deepest context +// from ErrCtx errors. If there is no wrapped ErrCtx RootCtxOrFallback returns ctx from parameter. +func RootCtxOrFallback(ctx context.Context, err error) context.Context { + var lastCtx context.Context + + type causer interface { + Cause() error + } + + // Unwrap each error to get the deepest context + for err != nil { + // First check if the err is type of `*errgo.Err` to be able to call `Underlying()` + // method. Both `*errgo.Err` and `*errors.Err` are implementing a causer interface. + // Cause() method from errgo skip all underlying errors, so we may skip a context between. + // So the order matter, we need to call `Cause()` after `Underlying()`. + errgoErr, ok := err.(*errgo.Err) + if ok { + err = errgoErr.Underlying() + continue + } + + cause, ok := err.(causer) + if ok { + err = cause.Cause() + continue + } + + // if err is type of `ErrCtx` unwrap it by getting errCtx.err + ctxerr, ok := err.(ErrCtx) + if ok { + err = ctxerr.err + lastCtx = ctxerr.Ctx() + + continue + } + + break + } + + if lastCtx == nil { + return ctx + } + + return lastCtx +} diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/errgo.go b/vendor/github.com/Scalingo/go-utils/errors/v2/errgo.go new file mode 100644 index 000000000..5a05a5fc0 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/errgo.go @@ -0,0 +1,22 @@ +package errors + +import ( + "gopkg.in/errgo.v1" +) + +func errgoRoot(err error) error { + for { + e, ok := err.(ErrCtx) + if ok { + err = e.err + } + errgoErr, ok := err.(*errgo.Err) + if !ok { + return err + } + if errgoErr.Underlying() == nil { + return err + } + err = errgoErr.Underlying() + } +} diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/errors.go b/vendor/github.com/Scalingo/go-utils/errors/v2/errors.go new file mode 100644 index 000000000..0300414b9 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/errors.go @@ -0,0 +1,21 @@ +package errors + +func errorCause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + e, ok := err.(ErrCtx) + if ok { + err = e.err + } + + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/Scalingo/go-utils/errors/v2/validation_errors.go b/vendor/github.com/Scalingo/go-utils/errors/v2/validation_errors.go new file mode 100644 index 000000000..0145f9c12 --- /dev/null +++ b/vendor/github.com/Scalingo/go-utils/errors/v2/validation_errors.go @@ -0,0 +1,96 @@ +package errors + +import ( + "fmt" + "strings" +) + +// ValidationErrors store each errors associated to every fields of a model +type ValidationErrors struct { + Errors map[string][]string `json:"errors"` +} + +func (v *ValidationErrors) Error() string { + var builder strings.Builder + index := 0 + + for field, errs := range v.Errors { + index++ + builder.WriteString(fmt.Sprintf("%s=%s", field, strings.Join(errs, ", "))) + if index < len(v.Errors) { + builder.WriteString(" ") + } + } + + return builder.String() +} + +// ValidationErrorsBuilder is used to provide a simple way to create a ValidationErrors struct. The typical usecase is: +// func (m *MyModel) Validate(ctx context.Context) *ValidationErrors { +// validations := document.NewValidationErrorsBuilder() +// +// if m.Name == "" { +// validations.Set("name", "should not be empty") +// } +// +// if m.Email == "" { +// validations.Set("email", "should not be empty") +// } +// +// return validations.Build() +// } +type ValidationErrorsBuilder struct { + errors map[string][]string +} + +// NewValidationErrors return an empty ValidationErrors struct +func NewValidationErrorsBuilder() *ValidationErrorsBuilder { + return &ValidationErrorsBuilder{ + errors: make(map[string][]string), + } +} + +// Set will add an error on a specific field, if the field already contains an error, it will just add it to the current errors list +func (v *ValidationErrorsBuilder) Set(field, err string) *ValidationErrorsBuilder { + v.errors[field] = append(v.errors[field], err) + return v +} + +// Get will return all errors set for a specific field +func (v *ValidationErrorsBuilder) Get(field string) []string { + return v.errors[field] +} + +// Merge ValidationErrors with another ValidationErrors +func (v *ValidationErrorsBuilder) Merge(verr *ValidationErrors) *ValidationErrorsBuilder { + return v.MergeWithPrefix("", verr) +} + +// MergeWithPrefix is merging ValidationErrors in another ValidationError +// adding a prefix for each error field +func (v *ValidationErrorsBuilder) MergeWithPrefix(prefix string, verr *ValidationErrors) *ValidationErrorsBuilder { + if verr == nil { + return v + } + if prefix != "" && prefix[len(prefix)-1] != '_' { + prefix = prefix + "_" + } + + for key, values := range verr.Errors { + for _, value := range values { + v.Set(prefix+key, value) + } + } + return v +} + +// Build will send a ValidationErrors struct if there is some errors or nil if no errors has been defined +func (v *ValidationErrorsBuilder) Build() *ValidationErrors { + if len(v.errors) == 0 { + return nil + } + + return &ValidationErrors{ + Errors: v.errors, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 43521c4d9..9c04d63a6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -31,8 +31,8 @@ github.com/ProtonMail/go-crypto/openpgp/s2k # github.com/ScaleFT/sshkeys v1.2.0 ## explicit; go 1.13 github.com/ScaleFT/sshkeys -# github.com/Scalingo/go-scalingo/v6 v6.3.0 -## explicit; go 1.17 +# github.com/Scalingo/go-scalingo/v6 v6.4.0 +## explicit; go 1.20 github.com/Scalingo/go-scalingo/v6 github.com/Scalingo/go-scalingo/v6/billing github.com/Scalingo/go-scalingo/v6/debug @@ -42,6 +42,9 @@ github.com/Scalingo/go-scalingo/v6/io # github.com/Scalingo/go-utils/errors v1.1.1 ## explicit; go 1.17 github.com/Scalingo/go-utils/errors +# github.com/Scalingo/go-utils/errors/v2 v2.2.0 +## explicit; go 1.17 +github.com/Scalingo/go-utils/errors/v2 # github.com/Scalingo/go-utils/logger v1.2.0 ## explicit; go 1.17 github.com/Scalingo/go-utils/logger From e36fbd4c9f3c4266f4b7b1a708cfb9e6a0770c52 Mon Sep 17 00:00:00 2001 From: Mederic Bazart Date: Thu, 16 Mar 2023 18:49:08 +0100 Subject: [PATCH 4/5] fix(operation): use new errors package --- apps/operations.go | 21 ++++++++++----------- go.mod | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/operations.go b/apps/operations.go index b97a686a2..b2310c592 100644 --- a/apps/operations.go +++ b/apps/operations.go @@ -9,11 +9,10 @@ import ( "path/filepath" "time" - "gopkg.in/errgo.v1" - "github.com/Scalingo/cli/config" "github.com/Scalingo/cli/io" - "github.com/Scalingo/go-scalingo/v6" + scalingo "github.com/Scalingo/go-scalingo/v6" + errors "github.com/Scalingo/go-utils/errors/v2" ) func handleOperation(ctx context.Context, app string, res *http.Response) error { @@ -24,12 +23,12 @@ func handleOperation(ctx context.Context, app string, res *http.Response) error func handleOperationWithURL(ctx context.Context, app string, operationURL string, containerLabel ...string) error { opURL, err := url.Parse(operationURL) if err != nil { - return errgo.Mask(err) + return errors.Notef(ctx, err, "parse url of operation") } c, err := config.ScalingoClient(ctx) if err != nil { - return errgo.Notef(err, "get Scalingo client") + return errors.Notef(ctx, err, "get Scalingo client") } var op *scalingo.Operation @@ -42,7 +41,7 @@ func handleOperationWithURL(ctx context.Context, app string, operationURL string op, err = c.OperationsShow(ctx, app, opID) if err != nil { - return errgo.Notef(err, "get operation") + return errors.Notef(ctx, err, "get operation %v", opID) } go func() { @@ -73,14 +72,14 @@ func handleOperationWithURL(ctx context.Context, app string, operationURL string for { select { case err := <-errs: - return errgo.Mask(err) + return errors.Notef(ctx, err, "get operation %v", op.ID) case <-done: if op.Status == "done" { fmt.Printf("\bDone in %.3f seconds\n", op.ElapsedDuration()) return nil } else if op.Status == "error" { fmt.Printf("\bOperation '%s' failed, an error occurred: %v\n", op.Type, op.Error) - return errgo.Newf("operation %v failed", op.ID) + return errors.Newf(ctx, "operation %v failed", op.ID) } } } @@ -89,19 +88,19 @@ func handleOperationWithURL(ctx context.Context, app string, operationURL string func GetAttachURLFromOperationWithURL(ctx context.Context, app string, operationURL string) (string, error) { opURL, err := url.Parse(operationURL) if err != nil { - return "", errgo.Mask(err) + return "", errors.Notef(ctx, err, "parse url of operation") } c, err := config.ScalingoClient(ctx) if err != nil { - return "", errgo.Notef(err, "get Scalingo client") + return "", errors.Notef(ctx, err, "get Scalingo client") } var operation *scalingo.Operation opID := filepath.Base(opURL.Path) operation, err = c.OperationsShow(ctx, app, opID) if err != nil { - return "", errgo.Notef(err, "get operation") + return "", errors.Notef(ctx, err, "get operation %v", opID) } return operation.StartOneOffData.AttachURL, nil diff --git a/go.mod b/go.mod index 482282dfd..a40cfc7f4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ScaleFT/sshkeys v1.2.0 github.com/Scalingo/go-scalingo/v6 v6.4.0 github.com/Scalingo/go-utils/errors v1.1.1 + github.com/Scalingo/go-utils/errors/v2 v2.2.0 github.com/Scalingo/go-utils/logger v1.2.0 github.com/Scalingo/go-utils/retry v1.1.1 github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 @@ -33,7 +34,6 @@ require ( require ( github.com/Microsoft/go-winio v0.6.0 // indirect github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect - github.com/Scalingo/go-utils/errors/v2 v2.2.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/cloudflare/circl v1.3.2 // indirect From 2c17bff9f99eac04ffa6a8795ccdd4f5b25b7119 Mon Sep 17 00:00:00 2001 From: Mederic Bazart Date: Thu, 16 Mar 2023 18:52:18 +0100 Subject: [PATCH 5/5] fix(one-off): fix linter errors --- apps/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/run.go b/apps/run.go index 95f5c957e..c1a78a8be 100644 --- a/apps/run.go +++ b/apps/run.go @@ -20,7 +20,7 @@ import ( "runtime" "strings" - "gopkg.in/errgo.v1" + errgo "gopkg.in/errgo.v1" "github.com/Scalingo/cli/apps/run" "github.com/Scalingo/cli/config"