mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-25 01:14:18 +00:00
Run the E2E test on kind / build (push) Failing after 7m47s
Run the E2E test on kind / setup-test-matrix (push) Successful in 4s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / Build (push) Failing after 39s
Close stale issues and PRs / stale (push) Successful in 20s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m39s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m21s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m28s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m28s
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
857 lines
31 KiB
Go
857 lines
31 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 velero
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/mod/semver"
|
|
appsv1api "k8s.io/api/apps/v1"
|
|
corev1api "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
clientset "k8s.io/client-go/kubernetes"
|
|
|
|
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
|
"github.com/vmware-tanzu/velero/pkg/cmd/cli/install"
|
|
velerexec "github.com/vmware-tanzu/velero/pkg/util/exec"
|
|
"github.com/vmware-tanzu/velero/test"
|
|
common "github.com/vmware-tanzu/velero/test/util/common"
|
|
eksutil "github.com/vmware-tanzu/velero/test/util/eks"
|
|
"github.com/vmware-tanzu/velero/test/util/k8s"
|
|
)
|
|
|
|
// we provide more install options other than the standard install.InstallOptions in E2E test
|
|
type installOptions struct {
|
|
*install.Options
|
|
RegistryCredentialFile string
|
|
RestoreHelperImage string
|
|
VeleroServerDebugMode bool
|
|
WithoutDisableInformerCacheParam bool
|
|
WorkerOS string
|
|
}
|
|
|
|
func VeleroInstall(ctx context.Context, veleroCfg *test.VeleroConfig, isStandbyCluster bool) error {
|
|
fmt.Printf("Velero install %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
|
|
|
// veleroCfg struct including a set of BSL params and a set of additional BSL params,
|
|
// additional BSL set is for additional BSL test only, so only default BSL set is effective
|
|
// for VeleroInstall().
|
|
//
|
|
// veleroCfg struct including 2 sets of cluster setting, but VeleroInstall() only read
|
|
// default cluster settings, so if E2E test needs install on the standby cluster, default cluster
|
|
// setting should be reset to the value of standby cluster's.
|
|
//
|
|
// Some other setting might not needed by standby cluster installation like "snapshotMoveData", because in
|
|
// standby cluster only restore if performed, so CSI plugin is not needed, but it is installed due to
|
|
// the only one veleroCfg setting is provided as current design, since it will not introduce any issues as
|
|
// we can predict, so keep it intact for now.
|
|
if isStandbyCluster {
|
|
veleroCfg.CloudProvider = veleroCfg.StandbyClusterCloudProvider
|
|
}
|
|
|
|
if slices.Contains(test.PublicCloudProviders, veleroCfg.CloudProvider) {
|
|
fmt.Println("For public cloud platforms, object store plugin provider will be set as cloud provider")
|
|
// If ObjectStoreProvider is not provided, then using the value same as CloudProvider
|
|
if veleroCfg.ObjectStoreProvider == "" {
|
|
veleroCfg.ObjectStoreProvider = veleroCfg.CloudProvider
|
|
}
|
|
} else {
|
|
if veleroCfg.ObjectStoreProvider == "" {
|
|
return errors.New("No object store provider specified - must be specified when using kind as the cloud provider") // Must have an object store provider
|
|
}
|
|
}
|
|
|
|
pluginsTmp, err := GetPlugins(ctx, *veleroCfg, true)
|
|
if err != nil {
|
|
return errors.WithMessage(err, "Failed to get provider plugins")
|
|
}
|
|
err = k8s.EnsureClusterExists(ctx)
|
|
if err != nil {
|
|
return errors.WithMessage(err, "Failed to ensure Kubernetes cluster exists")
|
|
}
|
|
|
|
// TODO - handle this better
|
|
if veleroCfg.CloudProvider == test.Vsphere {
|
|
// We overrider the ObjectStoreProvider here for vSphere because we want to use the aws plugin for the
|
|
// backup, but needed to pick up the provider plugins earlier. vSphere plugin no longer needs a Volume
|
|
// Snapshot location specified
|
|
if veleroCfg.ObjectStoreProvider == "" {
|
|
veleroCfg.ObjectStoreProvider = test.AWS
|
|
}
|
|
|
|
if err := cleanVSpherePluginConfig(
|
|
veleroCfg.ClientToInstallVelero.ClientGo,
|
|
veleroCfg.VeleroNamespace,
|
|
test.VeleroVSphereSecretName,
|
|
test.VeleroVSphereConfigMapName,
|
|
); err != nil {
|
|
return errors.WithMessagef(err, "Failed to clear up vsphere plugin config %s namespace", veleroCfg.VeleroNamespace)
|
|
}
|
|
if err := generateVSpherePlugin(veleroCfg); err != nil {
|
|
return errors.WithMessagef(err, "Failed to config vsphere plugin")
|
|
}
|
|
}
|
|
|
|
veleroInstallOptions, err := getProviderVeleroInstallOptions(veleroCfg, pluginsTmp)
|
|
if err != nil {
|
|
return errors.WithMessagef(err, "Failed to get Velero InstallOptions for plugin provider %s", veleroCfg.ObjectStoreProvider)
|
|
}
|
|
|
|
_, err = k8s.GetNamespace(ctx, *veleroCfg.ClientToInstallVelero, veleroCfg.VeleroNamespace)
|
|
// We should uninstall Velero for a new installation
|
|
if !apierrors.IsNotFound(err) {
|
|
if err := VeleroUninstall(context.Background(), *veleroCfg); err != nil {
|
|
return errors.Wrapf(err, "Failed to uninstall velero %s", veleroCfg.VeleroNamespace)
|
|
}
|
|
}
|
|
|
|
// If velero namespace does not exist, we should create it for service account creation
|
|
if err := k8s.KubectlCreateNamespace(ctx, veleroCfg.VeleroNamespace); err != nil {
|
|
return errors.Wrapf(err, "Failed to create namespace %s to install Velero", veleroCfg.VeleroNamespace)
|
|
}
|
|
|
|
// Create Backup Repository ConfigurationMap.
|
|
if _, err := k8s.CreateConfigMap(
|
|
veleroCfg.ClientToInstallVelero.ClientGo,
|
|
veleroCfg.VeleroNamespace,
|
|
test.BackupRepositoryConfigName,
|
|
nil,
|
|
map[string]string{
|
|
test.UploaderTypeKopia: "{\"cacheLimitMB\": 2048, \"fullMaintenanceInterval\": \"normalGC\"}",
|
|
},
|
|
); err != nil {
|
|
return errors.WithMessagef(err,
|
|
"Failed to create %s ConfigMap in %s namespace",
|
|
test.BackupRepositoryConfigName,
|
|
veleroCfg.VeleroNamespace,
|
|
)
|
|
}
|
|
|
|
// For AWS IRSA credential test, AWS IAM service account is required, so if ServiceAccountName and EKSPolicyARN
|
|
// are both provided, we assume IRSA test is running, otherwise skip this IAM service account creation part.
|
|
if veleroCfg.CloudProvider == test.AWS && veleroInstallOptions.ServiceAccountName != "" {
|
|
if veleroCfg.EKSPolicyARN == "" {
|
|
return errors.New("Please provide EKSPolicyARN for IRSA test.")
|
|
}
|
|
if err := k8s.KubectlDeleteClusterRoleBinding(ctx, "velero-cluster-role"); err != nil {
|
|
return errors.Wrapf(err, "Failed to delete clusterrolebinding %s to %s namespace", "velero-cluster-role", veleroCfg.VeleroNamespace)
|
|
}
|
|
if err := k8s.KubectlCreateClusterRoleBinding(ctx, "velero-cluster-role", "cluster-admin", veleroCfg.VeleroNamespace, veleroInstallOptions.ServiceAccountName); err != nil {
|
|
return errors.Wrapf(err, "Failed to create clusterrolebinding %s to %s namespace", "velero-cluster-role", veleroCfg.VeleroNamespace)
|
|
}
|
|
|
|
if err := eksutil.KubectlDeleteIAMServiceAcount(ctx, veleroInstallOptions.ServiceAccountName, veleroCfg.VeleroNamespace, veleroCfg.ClusterToInstallVelero); err != nil {
|
|
return errors.Wrapf(err, "Failed to delete service account %s to %s namespace", veleroInstallOptions.ServiceAccountName, veleroCfg.VeleroNamespace)
|
|
}
|
|
if err := eksutil.EksctlCreateIAMServiceAcount(ctx, veleroInstallOptions.ServiceAccountName, veleroCfg.VeleroNamespace, veleroCfg.EKSPolicyARN, veleroCfg.ClusterToInstallVelero); err != nil {
|
|
return errors.Wrapf(err, "Failed to create service account %s to %s namespace", veleroInstallOptions.ServiceAccountName, veleroCfg.VeleroNamespace)
|
|
}
|
|
}
|
|
|
|
if err := installVeleroServer(
|
|
ctx,
|
|
veleroCfg.VeleroCLI,
|
|
veleroCfg.VeleroVersion,
|
|
&installOptions{
|
|
Options: veleroInstallOptions,
|
|
RegistryCredentialFile: veleroCfg.RegistryCredentialFile,
|
|
RestoreHelperImage: veleroCfg.RestoreHelperImage,
|
|
VeleroServerDebugMode: veleroCfg.VeleroServerDebugMode,
|
|
WithoutDisableInformerCacheParam: veleroCfg.WithoutDisableInformerCacheParam,
|
|
WorkerOS: veleroCfg.WorkerOS,
|
|
},
|
|
); err != nil {
|
|
time.Sleep(1 * time.Minute)
|
|
RunDebug(context.Background(), veleroCfg.VeleroCLI, veleroCfg.VeleroNamespace, "", "")
|
|
return errors.WithMessagef(err, "Failed to install Velero in the cluster")
|
|
}
|
|
fmt.Printf("Finish velero install %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
|
return nil
|
|
}
|
|
|
|
// generateVSpherePlugin refers to
|
|
// https://github.com/vmware-tanzu/velero-plugin-for-vsphere/blob/v1.3.0/docs/vanilla.md
|
|
func generateVSpherePlugin(veleroCfg *test.VeleroConfig) error {
|
|
cli := veleroCfg.ClientToInstallVelero
|
|
|
|
if err := k8s.CreateNamespace(
|
|
context.Background(),
|
|
*cli,
|
|
veleroCfg.VeleroNamespace,
|
|
); err != nil {
|
|
return errors.WithMessagef(
|
|
err,
|
|
"Failed to create Velero %s namespace",
|
|
veleroCfg.VeleroNamespace,
|
|
)
|
|
}
|
|
|
|
clusterFlavor := "VANILLA"
|
|
|
|
if err := createVCCredentialSecret(cli.ClientGo, veleroCfg.VeleroNamespace); err != nil {
|
|
// For TKGs/uTKG the VC secret is not supposed to exist.
|
|
if apierrors.IsNotFound(err) {
|
|
clusterFlavor = "GUEST"
|
|
} else {
|
|
return errors.WithMessagef(
|
|
err,
|
|
"Failed to create virtual center credential secret in %s namespace",
|
|
veleroCfg.VeleroNamespace,
|
|
)
|
|
}
|
|
}
|
|
|
|
_, err := k8s.CreateConfigMap(
|
|
cli.ClientGo,
|
|
veleroCfg.VeleroNamespace,
|
|
test.VeleroVSphereConfigMapName,
|
|
nil,
|
|
map[string]string{
|
|
"cluster_flavor": clusterFlavor,
|
|
"vsphere_secret_name": test.VeleroVSphereSecretName,
|
|
"vsphere_secret_namespace": veleroCfg.VeleroNamespace,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return errors.WithMessagef(
|
|
err,
|
|
"Failed to create velero-vsphere-plugin-config ConfigMap in %s namespace",
|
|
veleroCfg.VeleroNamespace,
|
|
)
|
|
}
|
|
|
|
if err := k8s.WaitForConfigMapComplete(
|
|
cli.ClientGo,
|
|
veleroCfg.VeleroNamespace,
|
|
test.VeleroVSphereConfigMapName,
|
|
); err != nil {
|
|
return errors.Wrap(
|
|
err,
|
|
fmt.Sprintf("Failed to ensure ConfigMap %s completion in namespace: %s",
|
|
test.VeleroVSphereConfigMapName,
|
|
veleroCfg.VeleroNamespace,
|
|
),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func cleanVSpherePluginConfig(c clientset.Interface, ns, secretName, configMapName string) error {
|
|
//clear secret
|
|
_, err := k8s.GetSecret(c, ns, secretName)
|
|
if err == nil { //exist
|
|
if err := k8s.WaitForSecretDelete(c, ns, secretName); err != nil {
|
|
return errors.WithMessagef(err, "Failed to clear up vsphere plugin secret in %s namespace", ns)
|
|
}
|
|
} else if !apierrors.IsNotFound(err) {
|
|
return errors.WithMessagef(err, "Failed to retrieve vsphere plugin secret in %s namespace", ns)
|
|
}
|
|
|
|
//clear configmap
|
|
_, err = k8s.GetConfigMap(c, ns, configMapName)
|
|
if err == nil {
|
|
if err := k8s.WaitForConfigmapDelete(c, ns, configMapName); err != nil {
|
|
return errors.WithMessagef(err, "Failed to clear up vsphere plugin configmap in %s namespace", ns)
|
|
}
|
|
} else if !apierrors.IsNotFound(err) {
|
|
return errors.WithMessagef(err, "Failed to retrieve vsphere plugin configmap in %s namespace", ns)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func installVeleroServer(
|
|
ctx context.Context,
|
|
cli string,
|
|
version string,
|
|
options *installOptions,
|
|
) error {
|
|
args := []string{"install"}
|
|
namespace := "velero"
|
|
if len(options.Namespace) > 0 {
|
|
args = append(args, "--namespace", options.Namespace)
|
|
namespace = options.Namespace
|
|
}
|
|
if len(options.Image) > 0 {
|
|
args = append(args, "--image", options.Image)
|
|
}
|
|
if options.UseNodeAgent {
|
|
args = append(args, "--use-node-agent")
|
|
}
|
|
|
|
// TODO: need to consider align options.UseNodeAgentWindows usage
|
|
// with options.UseNodeAgent
|
|
// Only version after v1.16.0 support windows node agent.
|
|
if options.WorkerOS == common.WorkerOSWindows &&
|
|
(semver.Compare(version, "v1.16") >= 0 || version == "main") {
|
|
fmt.Println("Install node-agent-windows. The Velero version is ", version)
|
|
args = append(args, "--use-node-agent-windows")
|
|
}
|
|
|
|
if options.DefaultVolumesToFsBackup {
|
|
args = append(args, "--default-volumes-to-fs-backup")
|
|
}
|
|
if options.UseVolumeSnapshots {
|
|
args = append(args, "--use-volume-snapshots")
|
|
}
|
|
if len(options.ProviderName) > 0 {
|
|
args = append(args, "--provider", options.ProviderName)
|
|
}
|
|
if len(options.BackupStorageConfig.Data()) > 0 {
|
|
args = append(args, "--backup-location-config", options.BackupStorageConfig.String())
|
|
}
|
|
if len(options.BucketName) > 0 {
|
|
args = append(args, "--bucket", options.BucketName)
|
|
}
|
|
if len(options.Prefix) > 0 {
|
|
args = append(args, "--prefix", options.Prefix)
|
|
}
|
|
//Treat ServiceAccountName priority higher than SecretFile
|
|
if len(options.ServiceAccountName) > 0 {
|
|
args = append(args, "--service-account-name", options.ServiceAccountName)
|
|
} else {
|
|
if len(options.SecretFile) > 0 {
|
|
args = append(args, "--secret-file", options.SecretFile)
|
|
}
|
|
}
|
|
if options.NoSecret {
|
|
args = append(args, "--no-secret")
|
|
}
|
|
if len(options.VolumeSnapshotConfig.Data()) > 0 {
|
|
args = append(args, "--snapshot-location-config", options.VolumeSnapshotConfig.String())
|
|
}
|
|
if len(options.Plugins) > 0 {
|
|
args = append(args, "--plugins", options.Plugins.String())
|
|
}
|
|
|
|
if !options.WithoutDisableInformerCacheParam {
|
|
if options.DisableInformerCache {
|
|
args = append(args, "--disable-informer-cache=true")
|
|
} else {
|
|
args = append(args, "--disable-informer-cache=false")
|
|
}
|
|
}
|
|
|
|
if len(options.Features) > 0 {
|
|
args = append(args, "--features", options.Features)
|
|
}
|
|
|
|
if options.GarbageCollectionFrequency > 0 {
|
|
args = append(args, fmt.Sprintf("--garbage-collection-frequency=%v", options.GarbageCollectionFrequency))
|
|
}
|
|
|
|
if options.PodVolumeOperationTimeout > 0 {
|
|
args = append(args, fmt.Sprintf("--pod-volume-operation-timeout=%v", options.PodVolumeOperationTimeout))
|
|
}
|
|
|
|
if options.NodeAgentPodCPULimit != "" {
|
|
args = append(args, fmt.Sprintf("--node-agent-pod-cpu-limit=%v", options.NodeAgentPodCPULimit))
|
|
}
|
|
|
|
if options.NodeAgentPodCPURequest != "" {
|
|
args = append(args, fmt.Sprintf("--node-agent-pod-cpu-request=%v", options.NodeAgentPodCPURequest))
|
|
}
|
|
|
|
if options.NodeAgentPodMemLimit != "" {
|
|
args = append(args, fmt.Sprintf("--node-agent-pod-mem-limit=%v", options.NodeAgentPodMemLimit))
|
|
}
|
|
|
|
if options.NodeAgentPodMemRequest != "" {
|
|
args = append(args, fmt.Sprintf("--node-agent-pod-mem-request=%v", options.NodeAgentPodMemRequest))
|
|
}
|
|
|
|
if options.VeleroPodCPULimit != "" {
|
|
args = append(args, fmt.Sprintf("--velero-pod-cpu-limit=%v", options.VeleroPodCPULimit))
|
|
}
|
|
|
|
if options.VeleroPodCPURequest != "" {
|
|
args = append(args, fmt.Sprintf("--velero-pod-cpu-request=%v", options.VeleroPodCPURequest))
|
|
}
|
|
|
|
if options.VeleroPodMemLimit != "" {
|
|
args = append(args, fmt.Sprintf("--velero-pod-mem-limit=%v", options.VeleroPodMemLimit))
|
|
}
|
|
|
|
if options.VeleroPodMemRequest != "" {
|
|
args = append(args, fmt.Sprintf("--velero-pod-mem-request=%v", options.VeleroPodMemRequest))
|
|
}
|
|
|
|
if len(options.UploaderType) > 0 {
|
|
args = append(args, fmt.Sprintf("--uploader-type=%v", options.UploaderType))
|
|
}
|
|
|
|
if options.ItemBlockWorkerCount > 1 {
|
|
args = append(args, fmt.Sprintf("--item-block-worker-count=%d", options.ItemBlockWorkerCount))
|
|
}
|
|
|
|
if len(options.PodLabels.Data()) > 0 {
|
|
args = append(args, "--pod-labels", options.PodLabels.String())
|
|
}
|
|
|
|
if len(options.ServiceAccountAnnotations.Data()) > 0 {
|
|
args = append(args, "--sa-annotations", options.ServiceAccountAnnotations.String())
|
|
}
|
|
|
|
// Only version no older than v1.15 support --backup-repository-configmap.
|
|
if options.BackupRepoConfigMap != "" &&
|
|
(semver.Compare(version, "v1.15") >= 0 || version == "main") {
|
|
fmt.Println("Associate backup repository ConfigMap. The Velero version is ", version)
|
|
args = append(args, fmt.Sprintf("--backup-repository-configmap=%s", options.BackupRepoConfigMap))
|
|
}
|
|
|
|
if err := createVeleroResources(ctx, cli, namespace, args, options); err != nil {
|
|
return err
|
|
}
|
|
|
|
return waitVeleroReady(ctx, namespace, options.UseNodeAgent, options.UseNodeAgentWindows)
|
|
}
|
|
|
|
func createVeleroResources(ctx context.Context, cli, namespace string, args []string, options *installOptions) error {
|
|
args = append(args, "--dry-run", "--output", "json", "--crds-only")
|
|
|
|
// get the CRD definitions
|
|
cmd := exec.CommandContext(ctx, cli, args...)
|
|
fmt.Printf("Running cmd %q \n", cmd.String())
|
|
stdout, stderr, err := velerexec.RunCommand(cmd)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to run velero install dry run command with \"--crds-only\" option, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
|
|
// apply the CRDs first
|
|
cmd = exec.CommandContext(ctx, "kubectl", "apply", "-f", "-")
|
|
cmd.Stdin = bytes.NewBufferString(stdout)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
fmt.Printf("Applying velero CRDs...\n")
|
|
if err = cmd.Run(); err != nil {
|
|
return errors.Wrapf(err, "failed to apply the CRDs")
|
|
}
|
|
|
|
// wait until the CRDs are ready
|
|
cmd = exec.CommandContext(ctx, "kubectl", "wait", "--for", "condition=established", "-f", "-")
|
|
cmd.Stdin = bytes.NewBufferString(stdout)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
fmt.Printf("Waiting velero CRDs ready...\n")
|
|
if err = cmd.Run(); err != nil {
|
|
return errors.Wrapf(err, "failed to wait the CRDs be ready")
|
|
}
|
|
|
|
// Wait the Velero CRD API endpoint is ready
|
|
wait.PollUntilContextTimeout(ctx, k8s.PollInterval, time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
v1VerifyCmd := exec.CommandContext(ctx, "kubectl", "get", "--raw", "/apis/velero.io/v1")
|
|
v1VerifyCmd.Stdout = os.Stdout
|
|
v1VerifyCmd.Stderr = os.Stderr
|
|
if err := v1VerifyCmd.Run(); err != nil {
|
|
fmt.Printf("/apis/velero.io/v1 is not ready: %s.\n", err.Error())
|
|
return false, nil
|
|
}
|
|
|
|
v2alpha1VerifyCmd := exec.CommandContext(ctx, "kubectl", "get", "--raw", "/apis/velero.io/v2alpha1")
|
|
v2alpha1VerifyCmd.Stdout = os.Stdout
|
|
v2alpha1VerifyCmd.Stderr = os.Stderr
|
|
if err := v2alpha1VerifyCmd.Run(); err != nil {
|
|
fmt.Printf("/apis/velero.io/v2alpha1 is not ready: %s.\n", err.Error())
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// remove the "--crds-only" option from the args
|
|
args = args[:len(args)-1]
|
|
cmd = exec.CommandContext(ctx, cli, args...)
|
|
fmt.Printf("Running cmd %q \n", cmd.String())
|
|
stdout, stderr, err = velerexec.RunCommand(cmd)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to run velero install dry run command, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
|
|
// From v1.15, the Restic uploader is deprecated,
|
|
// and a warning message is printed for the install CLI.
|
|
// Need to skip the deprecation of Restic message before the generated JSON.
|
|
// Redirect to the stdout to the first curly bracket to skip the warning.
|
|
if stdout[0] != '{' {
|
|
newIndex := strings.Index(stdout, "{")
|
|
stdout = stdout[newIndex:]
|
|
}
|
|
|
|
resources := &unstructured.UnstructuredList{}
|
|
if err := json.Unmarshal([]byte(stdout), resources); err != nil {
|
|
return errors.Wrapf(err, "failed to unmarshal the resources: %s", stdout)
|
|
}
|
|
|
|
if err = patchResources(resources, namespace, options); err != nil {
|
|
return errors.Wrapf(err, "failed to patch resources")
|
|
}
|
|
|
|
data, err := json.Marshal(resources)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to marshal resources")
|
|
}
|
|
|
|
cmd = exec.CommandContext(ctx, "kubectl", "apply", "-f", "-")
|
|
cmd.Stdin = bytes.NewReader(data)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
fmt.Printf("Running cmd %q \n", cmd.String())
|
|
if err = cmd.Run(); err != nil {
|
|
return errors.Wrapf(err, "failed to apply Velero resources")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// patch the velero resources
|
|
func patchResources(resources *unstructured.UnstructuredList, namespace string, options *installOptions) error {
|
|
i := 0
|
|
size := 2
|
|
var deploy appsv1api.Deployment
|
|
var imagePullSecret corev1api.Secret
|
|
|
|
for resourceIndex, resource := range resources.Items {
|
|
// apply the image pull secret to avoid the image pull limit of Docker Hub
|
|
if len(options.RegistryCredentialFile) > 0 && resource.GetKind() == "ServiceAccount" &&
|
|
resource.GetName() == "velero" {
|
|
credential, err := os.ReadFile(options.RegistryCredentialFile)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to read the registry credential file %s", options.RegistryCredentialFile)
|
|
}
|
|
imagePullSecret = corev1api.Secret{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Secret",
|
|
APIVersion: corev1api.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "image-pull-secret",
|
|
Namespace: namespace,
|
|
},
|
|
Type: corev1api.SecretTypeDockerConfigJson,
|
|
Data: map[string][]byte{
|
|
".dockerconfigjson": credential,
|
|
},
|
|
}
|
|
resource.Object["imagePullSecrets"] = []map[string]any{
|
|
{
|
|
"name": "image-pull-secret",
|
|
},
|
|
}
|
|
resources.Items[resourceIndex] = resource
|
|
fmt.Printf("image pull secret %q set for velero serviceaccount \n", "image-pull-secret")
|
|
|
|
un, err := toUnstructured(imagePullSecret)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to convert pull secret to unstructure")
|
|
}
|
|
resources.Items = append(resources.Items, un)
|
|
i++
|
|
} else if options.VeleroServerDebugMode && resource.GetKind() == "Deployment" &&
|
|
resource.GetName() == "velero" {
|
|
deployJSONStr, err := json.Marshal(resource.Object)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to marshal velero deployment")
|
|
}
|
|
if err := json.Unmarshal(deployJSONStr, &deploy); err != nil {
|
|
return errors.Wrapf(err, "failed to unmarshal velero deployment")
|
|
}
|
|
veleroDeployIndex := -1
|
|
for containerIndex, container := range deploy.Spec.Template.Spec.Containers {
|
|
if container.Name == "velero" {
|
|
veleroDeployIndex = containerIndex
|
|
container.Args = append(container.Args, "--log-level", "debug")
|
|
break
|
|
}
|
|
}
|
|
if veleroDeployIndex >= 0 {
|
|
deploy.Spec.Template.Spec.Containers[veleroDeployIndex].Args = append(deploy.Spec.Template.Spec.Containers[veleroDeployIndex].Args, "--log-level")
|
|
deploy.Spec.Template.Spec.Containers[veleroDeployIndex].Args = append(deploy.Spec.Template.Spec.Containers[veleroDeployIndex].Args, "debug")
|
|
un, err := toUnstructured(deploy)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to unstructured velero deployment")
|
|
}
|
|
resources.Items = append(resources.Items, un)
|
|
resources.Items = append(resources.Items[:resourceIndex], resources.Items[resourceIndex+1:]...)
|
|
} else {
|
|
return errors.New("failed to get velero container in velero pod")
|
|
}
|
|
i++
|
|
}
|
|
if i == size {
|
|
break
|
|
}
|
|
}
|
|
|
|
// customize the restic restore helper image
|
|
if len(options.RestoreHelperImage) > 0 {
|
|
restoreActionConfig := corev1api.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "ConfigMap",
|
|
APIVersion: corev1api.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "restic-restore-action-config",
|
|
Namespace: namespace,
|
|
Labels: map[string]string{
|
|
"velero.io/plugin-config": "",
|
|
"velero.io/pod-volume-restore": "RestoreItemAction",
|
|
},
|
|
},
|
|
Data: map[string]string{
|
|
"image": options.RestoreHelperImage,
|
|
},
|
|
}
|
|
|
|
un, err := toUnstructured(restoreActionConfig)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to convert restore action config to unstructure")
|
|
}
|
|
resources.Items = append(resources.Items, un)
|
|
fmt.Printf("the restic restore helper image is set by the configmap %q \n", "restic-restore-action-config")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toUnstructured(res any) (unstructured.Unstructured, error) {
|
|
un := unstructured.Unstructured{}
|
|
data, err := json.Marshal(res)
|
|
if err != nil {
|
|
return un, err
|
|
}
|
|
err = json.Unmarshal(data, &un)
|
|
return un, err
|
|
}
|
|
|
|
func waitVeleroReady(ctx context.Context, namespace string, useNodeAgent bool, useNodeAgentWindows bool) error {
|
|
fmt.Println("Waiting for Velero deployment to be ready.")
|
|
// when doing upgrade by the "kubectl apply" the command "kubectl wait --for=condition=available deployment/velero -n velero --timeout=600s" returns directly
|
|
// use "rollout status" instead to avoid this. For more detail information, refer to https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#complete-deployment
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "rollout", "status",
|
|
"deployment/velero", "-n", namespace))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "fail to wait for the velero deployment ready, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
|
|
if useNodeAgent {
|
|
fmt.Println("Waiting for node-agent daemonset to be ready.")
|
|
err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "get", "daemonset/node-agent",
|
|
"-o", "json", "-n", namespace))
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to get the node-agent daemonset, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
daemonset := &appsv1api.DaemonSet{}
|
|
if err = json.Unmarshal([]byte(stdout), daemonset); err != nil {
|
|
return false, errors.Wrapf(err, "failed to unmarshal the node-agent daemonset")
|
|
}
|
|
if daemonset.Status.DesiredNumberScheduled == daemonset.Status.NumberAvailable {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "fail to wait for the node-agent ready")
|
|
}
|
|
}
|
|
|
|
if useNodeAgentWindows {
|
|
fmt.Println("Waiting for node-agent-windows DaemonSet to be ready.")
|
|
err := wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 1*time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "get", "DaemonSet/node-agent-windows",
|
|
"-o", "json", "-n", namespace))
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to get the node-agent-windows DaemonSet, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
ds := &appsv1api.DaemonSet{}
|
|
if err = json.Unmarshal([]byte(stdout), ds); err != nil {
|
|
return false, errors.Wrapf(err, "failed to unmarshal the node-agent-windows DaemonSet")
|
|
}
|
|
if ds.Status.DesiredNumberScheduled == ds.Status.NumberAvailable {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "fail to wait for the node-agent-windows ready")
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Velero is installed and ready to be tested in the %s namespace! ⛵ \n", namespace)
|
|
return nil
|
|
}
|
|
|
|
func IsVeleroReady(ctx context.Context, veleroCfg *test.VeleroConfig) (bool, error) {
|
|
namespace := veleroCfg.VeleroNamespace
|
|
useNodeAgent := veleroCfg.UseNodeAgent
|
|
if useNodeAgent {
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "get", "daemonset/node-agent",
|
|
"-o", "json", "-n", namespace))
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to get the node-agent daemonset, stdout=%s, stderr=%s", stdout, stderr)
|
|
} else {
|
|
daemonset := &appsv1api.DaemonSet{}
|
|
if err = json.Unmarshal([]byte(stdout), daemonset); err != nil {
|
|
return false, errors.Wrapf(err, "failed to unmarshal the node-agent daemonset")
|
|
}
|
|
if daemonset.Status.DesiredNumberScheduled != daemonset.Status.NumberAvailable {
|
|
return false, fmt.Errorf("the available number pod %d in node-agent daemonset not equal to scheduled number %d", daemonset.Status.NumberAvailable, daemonset.Status.DesiredNumberScheduled)
|
|
}
|
|
}
|
|
}
|
|
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "get", "deployment/velero",
|
|
"-o", "json", "-n", namespace))
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to get the velero deployment stdout=%s, stderr=%s", stdout, stderr)
|
|
} else {
|
|
deployment := &appsv1api.Deployment{}
|
|
if err = json.Unmarshal([]byte(stdout), deployment); err != nil {
|
|
return false, errors.Wrapf(err, "failed to unmarshal the velero deployment")
|
|
}
|
|
if deployment.Status.AvailableReplicas != deployment.Status.Replicas {
|
|
return false, fmt.Errorf("the available replicas %d in velero deployment not equal to replicas %d", deployment.Status.AvailableReplicas, deployment.Status.Replicas)
|
|
}
|
|
}
|
|
|
|
// Check BSL with poll
|
|
err = wait.PollUntilContextTimeout(ctx, k8s.PollInterval, time.Minute, true, func(ctx context.Context) (bool, error) {
|
|
return checkBSL(ctx, veleroCfg) == nil, nil
|
|
})
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "failed to check the bsl")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func checkBSL(ctx context.Context, veleroCfg *test.VeleroConfig) error {
|
|
namespace := veleroCfg.VeleroNamespace
|
|
stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(ctx, "kubectl", "get", "bsl", "default",
|
|
"-o", "json", "-n", namespace))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get bsl %s stdout=%s, stderr=%s", veleroCfg.BSLBucket, stdout, stderr)
|
|
} else {
|
|
bsl := &velerov1api.BackupStorageLocation{}
|
|
if err = json.Unmarshal([]byte(stdout), bsl); err != nil {
|
|
return errors.Wrapf(err, "failed to unmarshal the velero bsl")
|
|
}
|
|
if bsl.Status.Phase != velerov1api.BackupStorageLocationPhaseAvailable {
|
|
return fmt.Errorf("current bsl %s is not available", veleroCfg.BSLBucket)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func PrepareVelero(ctx context.Context, caseName string, veleroCfg test.VeleroConfig) error {
|
|
ready, err := IsVeleroReady(context.Background(), &veleroCfg)
|
|
if err != nil {
|
|
fmt.Printf("error in checking velero status with %v", err)
|
|
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Minute*5)
|
|
defer ctxCancel()
|
|
VeleroUninstall(ctx, veleroCfg)
|
|
ready = false
|
|
}
|
|
if ready {
|
|
fmt.Printf("velero is ready for case %s \n", caseName)
|
|
return nil
|
|
}
|
|
fmt.Printf("need to install velero for case %s \n", caseName)
|
|
return VeleroInstall(context.Background(), &veleroCfg, false)
|
|
}
|
|
|
|
func VeleroUninstall(ctx context.Context, veleroCfg test.VeleroConfig) error {
|
|
if stdout, stderr, err := velerexec.RunCommand(exec.CommandContext(
|
|
ctx,
|
|
veleroCfg.VeleroCLI,
|
|
"uninstall",
|
|
"--force",
|
|
"-n",
|
|
veleroCfg.VeleroNamespace,
|
|
)); err != nil {
|
|
return errors.Wrapf(err, "failed to uninstall velero, stdout=%s, stderr=%s", stdout, stderr)
|
|
}
|
|
fmt.Println("Velero uninstalled ⛵")
|
|
|
|
return nil
|
|
}
|
|
|
|
// createVCCredentialSecret refer to
|
|
// https://github.com/vmware-tanzu/velero-plugin-for-vsphere/blob/v1.3.0/docs/vanilla.md
|
|
func createVCCredentialSecret(c clientset.Interface, veleroNamespace string) error {
|
|
secret, err := getVCCredentialSecret(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vsphereCfg, exist := secret.Data["csi-vsphere.conf"]
|
|
if !exist {
|
|
return errors.New("failed to retrieve csi-vsphere config")
|
|
}
|
|
|
|
vsphereSecret := &corev1api.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "velero-vsphere-config-secret",
|
|
Namespace: veleroNamespace,
|
|
},
|
|
Type: corev1api.SecretTypeOpaque,
|
|
Data: map[string][]byte{"csi-vsphere.conf": vsphereCfg},
|
|
}
|
|
_, err = c.CoreV1().Secrets(veleroNamespace).Create(
|
|
context.TODO(),
|
|
vsphereSecret,
|
|
metav1.CreateOptions{},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := k8s.WaitForSecretsComplete(
|
|
c,
|
|
veleroNamespace,
|
|
test.VeleroVSphereSecretName,
|
|
); err != nil {
|
|
return errors.Wrap(
|
|
err,
|
|
"Failed to ensure velero-vsphere-config-secret secret completion in namespace kube-system",
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reference https://github.com/vmware-tanzu/velero-plugin-for-vsphere/blob/main/docs/vanilla.md#create-vc-credential-secret
|
|
// Read secret from kube-system namespace first, if not found, try with vmware-system-csi.
|
|
func getVCCredentialSecret(c clientset.Interface) (secret *corev1api.Secret, err error) {
|
|
secret, err = k8s.GetSecret(c, test.KubeSystemNamespace, "vsphere-config-secret")
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
secret, err = k8s.GetSecret(c, test.VSphereCSIControllerNamespace, "vsphere-config-secret")
|
|
}
|
|
}
|
|
return
|
|
}
|