Skip to content

Commit

Permalink
Retry deploy from URL in case of errors
Browse files Browse the repository at this point in the history
Skip ssl validation if the option is already provided
  • Loading branch information
IvanBorislavovDimitrov committed Dec 14, 2023
1 parent 5b43ea2 commit 06c4bbc
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 38 deletions.
8 changes: 4 additions & 4 deletions clients/cfrestclient/rest_cloud_foundry_client_extended.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package cfrestclient

import (
"code.cloudfoundry.org/cli/plugin"
"code.cloudfoundry.org/jsonry"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
"io"
"net/http"

"code.cloudfoundry.org/cli/plugin"
"code.cloudfoundry.org/jsonry"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
)

const cfBaseUrl = "v3/"
Expand Down Expand Up @@ -147,7 +148,6 @@ func getPaginatedResourcesWithIncluded[T any, Auxiliary any](url, token string,
func executeRequest(url, token string) ([]byte, error) {
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Add("Authorization", token)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
Expand Down
23 changes: 20 additions & 3 deletions clients/csrf/csrf_token_manager_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package csrf

import (
"crypto/tls"
"net"
"net/http"
"time"

"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/csrf/fakes"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"net/http"
"time"
)

const csrfTokenNotSet = ""
Expand Down Expand Up @@ -90,7 +93,21 @@ func expectCsrfTokenIsProperlySet(request *http.Request, csrfTokenHeader, csrfTo
}

func createTransport() *Transport {
return &Transport{Delegate: http.DefaultTransport.(*http.Transport),
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
delegateTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &Transport{Delegate: delegateTransport,
Csrf: &CsrfTokenHelper{NonProtectedMethods: getNonProtectedMethods()}}
}

Expand Down
14 changes: 12 additions & 2 deletions clients/mtaclient/mta_rest_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
const spacesURL string = "spaces/"
const restBaseURL string = "api/v1/"

const couldNotGetAsyncJobError = "could not get async file upload job"

type MtaRestClient struct {
baseclient.BaseClient
client *MtaClient
Expand All @@ -40,6 +42,7 @@ type AsyncUploadJobResult struct {
File *models.FileMetadata `json:"file,omitempty"`
MtaId string `json:"mta_id,omitempty"`
BytesProcessed int64 `json:"bytes_processed,omitempty"`
ClientActions []string `json:"client_actions,omitempty"`
}

func NewMtaClient(host, spaceID string, rt http.RoundTripper, tokenFactory baseclient.TokenFactory) MtaClientOperations {
Expand Down Expand Up @@ -383,12 +386,19 @@ func (c MtaRestClient) GetAsyncUploadJob(jobId string, namespace *string, appIns

resp, err := httpClient.Do(req)
if err != nil {
return AsyncUploadJobResult{}, fmt.Errorf("could not get async file upload job %s: %v", jobId, err)
return AsyncUploadJobResult{}, fmt.Errorf("%s %s: %v", couldNotGetAsyncJobError, jobId, err)
}
defer resp.Body.Close()

if resp.StatusCode/100 != 2 {
return AsyncUploadJobResult{}, fmt.Errorf("could not get async file upload job %s: %s", jobId, resp.Status)
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 400 {
return AsyncUploadJobResult{
ClientActions: []string{"RETRY_UPLOAD"},
}, fmt.Errorf("%s %s: %s, body: %s", couldNotGetAsyncJobError, jobId, resp.Status, string(bodyBytes))
}
return AsyncUploadJobResult{}, fmt.Errorf("%s %s: %s, body: %s", couldNotGetAsyncJobError, jobId, resp.Status, string(bodyBytes))

}

bodyBytes, err := io.ReadAll(resp.Body)
Expand Down
2 changes: 1 addition & 1 deletion clients/mtaclient/retryable_mta_rest_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func (c RetryableMtaRestClient) GetAsyncUploadJob(jobId string, namespace *strin
}
resp, err := baseclient.CallWithRetry(getAsyncUploadJobCb, c.MaxRetriesCount, c.RetryInterval)
if err != nil {
return AsyncUploadJobResult{}, err
return resp.(AsyncUploadJobResult), err
}
return resp.(AsyncUploadJobResult), nil
}
Expand Down
13 changes: 10 additions & 3 deletions commands/base_command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"crypto/tls"
"flag"
"fmt"
"io"
Expand Down Expand Up @@ -59,7 +60,12 @@ type BaseCommand struct {
// Initialize initializes the command with the specified name and CLI connection
func (c *BaseCommand) Initialize(name string, cliConnection plugin.CliConnection) {
log.Tracef("Initializing command %q\n", name)
transport := newTransport()
isSslDisabled, err := cliConnection.IsSSLDisabled()
if err != nil {
log.Tracef("Error while determining skip-ssl-validation: %v", err)
isSslDisabled = false
}
transport := newTransport(isSslDisabled)
tokenFactory := NewDefaultTokenFactory(cliConnection)
c.InitializeAll(name, cliConnection, transport, clients.NewDefaultClientFactory(), tokenFactory, util.NewDeployServiceURLCalculator(cliConnection))
}
Expand Down Expand Up @@ -264,11 +270,12 @@ func (c *BaseCommand) shouldAbortConflictingOperation(mtaID string, force bool)
terminal.EntityNameColor(mtaID))
}

func newTransport() http.RoundTripper {
func newTransport(isSslDisabled bool) http.RoundTripper {
csrfx := csrf.CsrfTokenHelper{NonProtectedMethods: getNonProtectedMethods()}
httpTransport := http.DefaultTransport.(*http.Transport)
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
// Increase tls handshake timeout to cope with slow internet connections. 3 x default value =30s.
httpTransport.TLSHandshakeTimeout = 30 * time.Second
httpTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: isSslDisabled}
return &csrf.Transport{Delegate: httpTransport, Csrf: &csrfx}
}

Expand Down
99 changes: 74 additions & 25 deletions commands/deploy_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,10 @@ package commands

import (
"bufio"
"code.cloudfoundry.org/cli/cf/terminal"
"code.cloudfoundry.org/cli/plugin"
"encoding/base64"
"errors"
"flag"
"fmt"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/configuration"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/log"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/util"
"gopkg.in/cheggaaa/pb.v1"
"io/fs"
"net/http"
"net/url"
Expand All @@ -24,6 +14,18 @@ import (
"strconv"
"strings"
"time"

"code.cloudfoundry.org/cli/cf/terminal"
"code.cloudfoundry.org/cli/plugin"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/baseclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands/retrier"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/configuration"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/log"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui"
"github.com/cloudfoundry-incubator/multiapps-cli-plugin/util"
"gopkg.in/cheggaaa/pb.v1"
)

const (
Expand Down Expand Up @@ -224,12 +226,12 @@ func (c *DeployCommand) executeInternal(positionalArgs []string, dsHost string,

if isUrl {
var fileId string
var isFailure bool
fileId, mtaId, isFailure = c.uploadFromUrl(mtaArchive, mtaClient, namespace, disableProgressBar)
if isFailure {

asyncUploadJobResult := c.uploadFromUrl(mtaArchive, mtaClient, namespace, disableProgressBar)
if asyncUploadJobResult.ExecutionStatus == Failure {
return Failure
}

mtaId, fileId = asyncUploadJobResult.MtaId, asyncUploadJobResult.FileId
// Check for an ongoing operation for this MTA ID and abort it
wasAborted, err := c.CheckOngoingOperation(mtaId, namespace, dsHost, force, cfTarget)
if err != nil {
Expand Down Expand Up @@ -330,14 +332,28 @@ func parseMtaArchiveArgument(rawMtaArchive interface{}) (bool, string) {
}

func (c *DeployCommand) uploadFromUrl(url string, mtaClient mtaclient.MtaClientOperations, namespace string,
disableProgressBar bool) (fileId, mtaId string, failure bool) {
progressBar := c.tryFetchMtarSize(url, disableProgressBar)

disableProgressBar bool) UploadFromUrlStatus {
encodedFileUrl := base64.URLEncoding.EncodeToString([]byte(url))
uploadStatus, _ := retrier.Execute[UploadFromUrlStatus](3, func() (UploadFromUrlStatus, error) {
progressBar := c.tryFetchMtarSize(url, disableProgressBar)
uploadFromUrlStatus := c.doUploadFromUrl(encodedFileUrl, mtaClient, namespace, progressBar)
return uploadFromUrlStatus, nil
}, func(result UploadFromUrlStatus, err error) bool {
return shouldRetryUpload(result)
})
return uploadStatus
}

func (c *DeployCommand) doUploadFromUrl(encodedFileUrl string, mtaClient mtaclient.MtaClientOperations, namespace string, progressBar *pb.ProgressBar) UploadFromUrlStatus {
responseHeaders, err := mtaClient.StartUploadMtaArchiveFromUrl(encodedFileUrl, &namespace)
if err != nil {
ui.Failed("Could not upload from url: %s", err)
return "", "", true
return UploadFromUrlStatus{
FileId: "",
MtaId: "",
ClientActions: make([]string, 0),
ExecutionStatus: Failure,
}
}

var totalBytesProcessed int64 = 0
Expand All @@ -356,17 +372,27 @@ func (c *DeployCommand) uploadFromUrl(url string, mtaClient mtaclient.MtaClientO
defer ticker.Stop()

var file *models.FileMetadata
var jobResult mtaclient.AsyncUploadJobResult
for file == nil {
jobResult, err := mtaClient.GetAsyncUploadJob(jobId, &namespace, responseHeaders.Get("x-cf-app-instance"))
if err != nil {
ui.Failed("Could not upload from url: %s", err)
return "", "", true
return UploadFromUrlStatus{
FileId: "",
MtaId: "",
ClientActions: jobResult.ClientActions,
ExecutionStatus: Failure,
}
}
file, mtaId = jobResult.File, jobResult.MtaId

file = jobResult.File
if len(jobResult.Error) != 0 {
ui.Failed("Async upload job failed: %s", jobResult.Error)
return "", "", true
return UploadFromUrlStatus{
FileId: "",
MtaId: "",
ClientActions: jobResult.ClientActions,
ExecutionStatus: Failure,
}
}

if progressBar != nil && jobResult.BytesProcessed != -1 {
Expand All @@ -379,19 +405,42 @@ func (c *DeployCommand) uploadFromUrl(url string, mtaClient mtaclient.MtaClientO
totalBytesProcessed = jobResult.BytesProcessed
}

if len(mtaId) == 0 {
if len(jobResult.MtaId) == 0 {
select {
case <-timeout.C:
ui.Failed("Upload from URL timed out after 1 hour")
return "", "", true
return UploadFromUrlStatus{
FileId: "",
MtaId: "",
ClientActions: make([]string, 0),
ExecutionStatus: Failure,
}
case <-ticker.C:
}
}
}
if progressBar != nil && totalBytesProcessed < progressBar.Total {
progressBar.Add64(progressBar.Total - totalBytesProcessed)
}
return file.ID, mtaId, false
return UploadFromUrlStatus{
FileId: file.ID,
MtaId: jobResult.MtaId,
ClientActions: jobResult.ClientActions,
ExecutionStatus: Success,
}
}

func shouldRetryUpload(uploadFromUrlStatus UploadFromUrlStatus) bool {
if uploadFromUrlStatus.ExecutionStatus == Success {
return false
}
for _, clientAction := range uploadFromUrlStatus.ClientActions {
if clientAction == "RETRY_UPLOAD" {
ui.Warn("Upload request must be retried")
return true
}
}
return false
}

func (c *DeployCommand) uploadFiles(files []string, fileUploader *FileUploader) ([]string, ExecutionStatus) {
Expand Down
29 changes: 29 additions & 0 deletions commands/retrier/retry_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package retrier

import (
"time"

"github.com/cloudfoundry-incubator/multiapps-cli-plugin/log"
)

func Execute[T any](attempts int, callback func() (T, error), shouldRetry func(result T, err error) bool) (T, error) {
var result T
var err error
for i := 0; i < attempts; i++ {
result, err = callback()
if shouldRetry(result, err) {
logError[T](result, err)
time.Sleep(3 * time.Second)
continue
}
return result, nil
}
return callback()
}

func logError[T any](result T, err error) {
if err != nil {
log.Tracef("retrying an operation that failed with: %v", err)
}
log.Tracef("result of the callback %v", result)
}
8 changes: 8 additions & 0 deletions commands/upload_from_url_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package commands

type UploadFromUrlStatus struct {
FileId string
MtaId string
ClientActions []string
ExecutionStatus ExecutionStatus
}

0 comments on commit 06c4bbc

Please sign in to comment.