Files
velero/pkg/cmd/util/downloadrequest/downloadrequest.go
T
Xun Jiang 49b670a791 Replace github.com/pkg/errors by github.com/cockroachdb/errors
Change errors.Cause to errors.Is, because github.com/cockroachdb/errors
New() function create a error with error stack with depth 1, but
github.com/pkg/errors's New() function create error with no depth.

Signed-off-by: Xun Jiang <xun.jiang@broadcom.com>
2026-06-10 15:55:57 +08:00

219 lines
6.1 KiB
Go

/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package downloadrequest
import (
"compress/gzip"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
"github.com/cockroachdb/errors"
"github.com/google/uuid"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
veleroV1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
)
// ErrNotFound is exported for external packages to check for when a file is
// not found
var ErrNotFound = errors.New("file not found")
var ErrDownloadRequestDownloadURLTimeout = errors.New("download request download url timeout, check velero server logs for errors. backup storage location may not be available")
func Stream(
ctx context.Context,
kbClient kbclient.Client,
namespace, name string,
kind veleroV1api.DownloadTargetKind,
w io.Writer,
timeout time.Duration,
insecureSkipTLSVerify bool,
caCertFile string,
) error {
return StreamWithBSLCACert(ctx, kbClient, namespace, name, kind, w, timeout, insecureSkipTLSVerify, caCertFile, "")
}
// StreamWithBSLCACert is like Stream but accepts an additional bslCACert parameter
// that contains the cacert from the BackupStorageLocation config
func StreamWithBSLCACert(
ctx context.Context,
kbClient kbclient.Client,
namespace, name string,
kind veleroV1api.DownloadTargetKind,
w io.Writer,
timeout time.Duration,
insecureSkipTLSVerify bool,
caCertFile string,
bslCACert string,
) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
downloadURL, err := getDownloadURL(ctx, kbClient, namespace, name, kind)
if err != nil {
return err
}
if err := download(ctx, downloadURL, kind, w, insecureSkipTLSVerify, caCertFile, bslCACert); err != nil {
return err
}
return nil
}
func getDownloadURL(
ctx context.Context,
kbClient kbclient.Client,
namespace, name string,
kind veleroV1api.DownloadTargetKind,
) (string, error) {
uuid, err := uuid.NewRandom()
if err != nil {
return "", err
}
reqName := fmt.Sprintf("%s-%s", name, uuid.String())
created := builder.ForDownloadRequest(namespace, reqName).Target(kind, name).Result()
if err := kbClient.Create(ctx, created, &kbclient.CreateOptions{}); err != nil {
return "", errors.WithStack(err)
}
for {
select {
case <-ctx.Done():
return "", ErrDownloadRequestDownloadURLTimeout
case <-time.After(25 * time.Millisecond):
updated := &veleroV1api.DownloadRequest{}
if err := kbClient.Get(ctx, kbclient.ObjectKey{Name: created.Name, Namespace: namespace}, updated); err != nil {
return "", errors.WithStack(err)
}
if updated.Status.DownloadURL != "" {
return updated.Status.DownloadURL, nil
}
}
}
}
func download(
ctx context.Context,
downloadURL string,
kind veleroV1api.DownloadTargetKind,
w io.Writer,
insecureSkipTLSVerify bool,
caCertFile string,
caCertByteString string,
) error {
var caPool *x509.CertPool
var err error
// Initialize caPool once
caPool, err = x509.SystemCertPool()
if err != nil {
caPool = x509.NewCertPool()
}
// Try to load CA cert from file first
if len(caCertFile) > 0 {
caCert, err := os.ReadFile(caCertFile)
if err != nil {
// If caCertFile fails and BSL cert is available, fall back to it
if len(caCertByteString) > 0 {
fmt.Fprintf(os.Stderr, "Warning: Failed to open CA certificate file %s: %v. Using CA certificate from backup storage location instead.\n", caCertFile, err)
caPool.AppendCertsFromPEM([]byte(caCertByteString))
} else {
// If no BSL cert available, return the original error
return errors.Wrapf(err, "couldn't open cacert")
}
} else {
caPool.AppendCertsFromPEM(caCert)
}
} else if len(caCertByteString) > 0 {
// If no caCertFile specified, use BSL cert if available
caPool.AppendCertsFromPEM([]byte(caCertByteString))
}
defaultTransport := http.DefaultTransport.(*http.Transport)
// same settings as the default transport
// aside from TLSClientConfig
httpClient := new(http.Client)
httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecureSkipTLSVerify, //nolint:gosec // This parameter is useful for some scenarios.
RootCAs: caPool,
},
DialContext: defaultTransport.DialContext,
ForceAttemptHTTP2: defaultTransport.ForceAttemptHTTP2,
MaxIdleConns: defaultTransport.MaxIdleConns,
Proxy: defaultTransport.Proxy,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil)
if err != nil {
return err
}
resp, err := httpClient.Do(httpReq)
if err != nil {
if urlErr, ok := err.(*url.Error); ok {
if _, ok := urlErr.Err.(x509.UnknownAuthorityError); ok {
return fmt.Errorf("%s\n\nThe --insecure-skip-tls-verify flag can also be used to accept any TLS certificate for the download, but it is susceptible to man-in-the-middle attacks", err.Error())
}
}
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrapf(err, "request failed: unable to decode response body")
}
if resp.StatusCode == http.StatusNotFound {
return ErrNotFound
}
return errors.Errorf("request failed: %v", string(body))
}
reader := resp.Body
if kind != veleroV1api.DownloadTargetKindBackupContents {
// need to decompress logs
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer gzipReader.Close()
reader = gzipReader
}
_, err = io.Copy(w, reader)
return err
}