mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-06 21:36:30 +00:00
Add velero install command (#1287)
Add velero install command Signed-off-by: Nolan Brubaker <brubakern@vmware.com>
This commit is contained in:
committed by
KubeKween
parent
bc8f07f963
commit
6f474016a6
1
changelogs/unreleased/1287-nrb
Normal file
1
changelogs/unreleased/1287-nrb
Normal file
@@ -0,0 +1 @@
|
||||
Add velero install command for basic use cases.
|
||||
219
pkg/cmd/cli/install/install.go
Normal file
219
pkg/cmd/cli/install/install.go
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
Copyright 2019 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 install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
"github.com/heptio/velero/pkg/client"
|
||||
"github.com/heptio/velero/pkg/cmd"
|
||||
"github.com/heptio/velero/pkg/cmd/util/flag"
|
||||
"github.com/heptio/velero/pkg/cmd/util/output"
|
||||
"github.com/heptio/velero/pkg/install"
|
||||
)
|
||||
|
||||
// InstallOptions collects all the options for installing Velero into a Kubernetes cluster.
|
||||
type InstallOptions struct {
|
||||
Namespace string
|
||||
Image string
|
||||
BucketName string
|
||||
Prefix string
|
||||
ProviderName string
|
||||
RestoreOnly bool
|
||||
SecretFile string
|
||||
DryRun bool
|
||||
BackupStorageConfig flag.Map
|
||||
VolumeSnapshotConfig flag.Map
|
||||
UseRestic bool
|
||||
Wait bool
|
||||
}
|
||||
|
||||
// BindFlags adds command line values to the options struct.
|
||||
func (o *InstallOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(&o.ProviderName, "provider", o.ProviderName, "provider name for backup and volume storage")
|
||||
flags.StringVar(&o.BucketName, "bucket", o.BucketName, "name of the object storage bucket where backups should be stored")
|
||||
flags.StringVar(&o.SecretFile, "secret-file", o.SecretFile, "file containing credentials for backup and volume provider")
|
||||
flags.StringVar(&o.Image, "image", o.Image, "image to use for the Velero and restic server pods. Optional.")
|
||||
flags.StringVar(&o.Prefix, "prefix", o.Prefix, "prefix under which all Velero data should be stored within the bucket. Optional.")
|
||||
flags.Var(&o.BackupStorageConfig, "backup-location-config", "configuration to use for the backup storage location. Format is key1=value1,key2=value2")
|
||||
flags.Var(&o.VolumeSnapshotConfig, "snapshot-location-config", "configuration to use for the volume snapshot location. Format is key1=value1,key2=value2")
|
||||
flags.BoolVar(&o.RestoreOnly, "restore-only", o.RestoreOnly, "run the server in restore-only mode. Optional.")
|
||||
flags.BoolVar(&o.DryRun, "dry-run", o.DryRun, "generate resources, but don't send them to the cluster. Use with -o. Optional.")
|
||||
flags.BoolVar(&o.UseRestic, "use-restic", o.UseRestic, "create restic deployment. Optional.")
|
||||
flags.BoolVar(&o.Wait, "wait", o.Wait, "wait for Velero deployment to be ready. Optional.")
|
||||
}
|
||||
|
||||
// NewInstallOptions instantiates a new, default InstallOptions stuct.
|
||||
func NewInstallOptions() *InstallOptions {
|
||||
return &InstallOptions{
|
||||
Namespace: api.DefaultNamespace,
|
||||
Image: install.DefaultImage,
|
||||
BackupStorageConfig: flag.NewMap(),
|
||||
VolumeSnapshotConfig: flag.NewMap(),
|
||||
}
|
||||
}
|
||||
|
||||
// AsVeleroOptions translates the values provided at the command line into values used to instantiate Kubernetes resources
|
||||
func (o *InstallOptions) AsVeleroOptions() (*install.VeleroOptions, error) {
|
||||
realPath, err := filepath.Abs(o.SecretFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secretData, err := ioutil.ReadFile(realPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &install.VeleroOptions{
|
||||
Namespace: o.Namespace,
|
||||
Image: o.Image,
|
||||
ProviderName: o.ProviderName,
|
||||
Bucket: o.BucketName,
|
||||
Prefix: o.Prefix,
|
||||
SecretData: secretData,
|
||||
RestoreOnly: o.RestoreOnly,
|
||||
UseRestic: o.UseRestic,
|
||||
BSLConfig: o.BackupStorageConfig.Data(),
|
||||
VSLConfig: o.VolumeSnapshotConfig.Data(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCommand creates a cobra command.
|
||||
func NewCommand(f client.Factory) *cobra.Command {
|
||||
o := NewInstallOptions()
|
||||
c := &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install Velero",
|
||||
Long: `
|
||||
Install Velero onto a Kubernetes cluster using the supplied provider information, such as
|
||||
the provider's name, a bucket name, and a file containing the credentials to access that bucket.
|
||||
A prefix within the bucket and configuration for the backup store location may also be supplied.
|
||||
Additionally, volume snapshot information for the same provider may be supplied.
|
||||
|
||||
All required CustomResourceDefinitions will be installed to the server, as well as the
|
||||
Velero Deployment and associated Restic DaemonSet.
|
||||
|
||||
The provided secret data will be created in a Secret named 'cloud-credentials'.
|
||||
|
||||
All namespaced resources will be placed in the 'velero' namespace.
|
||||
|
||||
Use '--wait' to wait for the Velero Deployment to be ready before proceeding.
|
||||
|
||||
Use '-o yaml' or '-o json' with '--dry-run' to output all generated resources as text instead of sending the resources to the server.
|
||||
This is useful as a starting point for more customized installations.
|
||||
`,
|
||||
Example: ` # velero install --bucket mybucket --provider gcp --secret-file ./gcp-service-account.json
|
||||
|
||||
# velero install --bucket backups --provider aws --secret-file ./aws-iam-creds --backup-location-config region=us-east-2 --snapshot-location-config region=us-east-2
|
||||
|
||||
# velero install --bucket backups --provider aws --secret-file ./aws-iam-creds --backup-location-config region=us-east-2 --snapshot-location-config region=us-east-2 --use-restic
|
||||
|
||||
# velero install --bucket gcp-backups --provider gcp --secret-file ./gcp-creds.json --wait
|
||||
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cmd.CheckError(o.Validate(c, args, f))
|
||||
cmd.CheckError(o.Complete(args, f))
|
||||
cmd.CheckError(o.Run(c))
|
||||
},
|
||||
}
|
||||
|
||||
o.BindFlags(c.Flags())
|
||||
output.BindFlags(c.Flags())
|
||||
output.ClearOutputFlagDefault(c)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Run executes a command in the context of the provided arguments.
|
||||
func (o *InstallOptions) Run(c *cobra.Command) error {
|
||||
vo, err := o.AsVeleroOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resources, err := install.AllResources(vo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := output.PrintWithFormat(c, resources); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.DryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
clientConfig, err := client.Config("", "", fmt.Sprintf("%s-%s", c.Parent().Name(), c.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
factory := client.NewDynamicFactory(dynamicClient)
|
||||
|
||||
err = install.Install(factory, resources, os.Stdout)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "\n\nError installing Velero. Use `kubectl logs deploy/velero -n velero` to check the deploy logs")
|
||||
}
|
||||
|
||||
if o.Wait {
|
||||
fmt.Println("Waiting for Velero to be ready.")
|
||||
if _, err = install.DeploymentIsReady(factory); err != nil {
|
||||
return errors.Wrap(err, "\n\nError installing Velero. Use `kubectl logs deploy/velero -n velero` to check the deploy logs")
|
||||
}
|
||||
}
|
||||
fmt.Println("Velero is installed! ⛵ Use 'kubectl logs deployment/velero -n velero' to view the status.")
|
||||
return nil
|
||||
}
|
||||
|
||||
//Complete completes options for a command.
|
||||
func (o *InstallOptions) Complete(args []string, f client.Factory) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates options provided to a command.
|
||||
func (o *InstallOptions) Validate(c *cobra.Command, args []string, f client.Factory) error {
|
||||
if err := output.ValidateFlags(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.BucketName == "" {
|
||||
return errors.New("--bucket is required")
|
||||
}
|
||||
|
||||
if o.ProviderName == "" {
|
||||
return errors.New("--provider is required")
|
||||
}
|
||||
|
||||
if o.SecretFile == "" {
|
||||
return errors.New("--secret-file is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -37,7 +37,7 @@ const downloadRequestTimeout = 30 * time.Second
|
||||
// BindFlags defines a set of output-specific flags within the provided
|
||||
// FlagSet.
|
||||
func BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP("output", "o", "table", "Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'.")
|
||||
flags.StringP("output", "o", "table", "Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'. 'table' is not valid for the install command.")
|
||||
labelColumns := flag.NewStringArray()
|
||||
flags.Var(&labelColumns, "label-columns", "a comma-separated list of labels to be displayed as columns")
|
||||
flags.Bool("show-labels", false, "show labels in the last column")
|
||||
@@ -84,7 +84,11 @@ func ValidateFlags(cmd *cobra.Command) error {
|
||||
func validateOutputFlag(cmd *cobra.Command) error {
|
||||
output := GetOutputFlagValue(cmd)
|
||||
switch output {
|
||||
case "", "table", "json", "yaml":
|
||||
case "", "json", "yaml":
|
||||
case "table":
|
||||
if cmd.Name() == "install" {
|
||||
return errors.New("'table' format is not supported with 'install' command")
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("invalid output format %q - valid values are 'table', 'json', and 'yaml'", output)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/heptio/velero/pkg/cmd/cli/delete"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/describe"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/get"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/install"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/plugin"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/restic"
|
||||
"github.com/heptio/velero/pkg/cmd/cli/restore"
|
||||
@@ -64,6 +65,7 @@ operations can also be performed as 'velero backup get' and 'velero schedule cre
|
||||
server.NewCommand(),
|
||||
version.NewCommand(f),
|
||||
get.NewCommand(f),
|
||||
install.NewCommand(f),
|
||||
describe.NewCommand(f),
|
||||
create.NewCommand(f),
|
||||
runplugin.NewCommand(f),
|
||||
|
||||
@@ -39,7 +39,8 @@ func CRDs() []*apiextv1beta1.CustomResourceDefinition {
|
||||
func crd(kind, plural string) *apiextv1beta1.CustomResourceDefinition {
|
||||
return &apiextv1beta1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s.%s", plural, velerov1api.GroupName),
|
||||
Name: fmt.Sprintf("%s.%s", plural, velerov1api.GroupName),
|
||||
Labels: labels(),
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CustomResourceDefinition",
|
||||
|
||||
@@ -58,7 +58,8 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1.DaemonSet {
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"name": "restic",
|
||||
"name": "restic",
|
||||
"component": "velero",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
@@ -87,6 +88,14 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1.DaemonSet {
|
||||
Name: "restic",
|
||||
Image: c.image,
|
||||
ImagePullPolicy: pullPolicy,
|
||||
Command: []string{
|
||||
"/velero",
|
||||
},
|
||||
Args: []string{
|
||||
"restic",
|
||||
"server",
|
||||
},
|
||||
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "host-pods",
|
||||
|
||||
260
pkg/install/install.go
Normal file
260
pkg/install/install.go
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
Copyright 2019 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 install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
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/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
|
||||
"github.com/heptio/velero/pkg/client"
|
||||
)
|
||||
|
||||
// kindToResource translates a Kind (mixed case, singular) to a Resource (lowercase, plural) string.
|
||||
// This is to accomodate the dynamic client's need for an APIResource, as the Unstructured objects do not have easy helpers for this information.
|
||||
var kindToResource = map[string]string{
|
||||
"CustomResourceDefinition": "customresourcedefinitions",
|
||||
"Namespace": "namespaces",
|
||||
"ClusterRoleBinding": "clusterrolebindings",
|
||||
"ServiceAccount": "serviceaccounts",
|
||||
"Deployment": "deployments",
|
||||
"DaemonSet": "daemonsets",
|
||||
"Secret": "secrets",
|
||||
"BackupStorageLocation": "backupstoragelocations",
|
||||
"VolumeSnapshotLocation": "volumesnapshotlocations",
|
||||
}
|
||||
|
||||
// ResourceGroup represents a collection of kubernetes objects with a common ready conditon
|
||||
type ResourceGroup struct {
|
||||
CRDResources []*unstructured.Unstructured
|
||||
OtherResources []*unstructured.Unstructured
|
||||
}
|
||||
|
||||
// crdIsReady checks a CRD to see if it's ready, so that objects may be created from it.
|
||||
func crdIsReady(crd *apiextv1beta1.CustomResourceDefinition) bool {
|
||||
var isEstablished, namesAccepted bool
|
||||
for _, cond := range crd.Status.Conditions {
|
||||
if cond.Type == apiextv1beta1.Established {
|
||||
isEstablished = true
|
||||
}
|
||||
if cond.Type == apiextv1beta1.NamesAccepted {
|
||||
namesAccepted = true
|
||||
}
|
||||
}
|
||||
|
||||
return (isEstablished && namesAccepted)
|
||||
}
|
||||
|
||||
// crdsAreReady polls the API server to see if the BackupStorageLocation and VolumeSnapshotLocation CRDs are ready to create objects.
|
||||
func crdsAreReady(factory client.DynamicFactory, crdKinds []string) (bool, error) {
|
||||
gvk := schema.FromAPIVersionAndKind(apiextv1beta1.SchemeGroupVersion.String(), "CustomResourceDefinition")
|
||||
apiResource := metav1.APIResource{
|
||||
Name: kindToResource["CustomResourceDefinition"],
|
||||
Namespaced: false,
|
||||
}
|
||||
c, err := factory.ClientForGroupVersionResource(gvk.GroupVersion(), apiResource, "")
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Error creating client for CustomResourceDefinition polling")
|
||||
}
|
||||
// Track all the CRDs that have been found and successfully marshalled.
|
||||
// len should be equal to len(crdKinds) in the happy path.
|
||||
foundCRDs := make([]*apiextv1beta1.CustomResourceDefinition, 0)
|
||||
var areReady bool
|
||||
err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) {
|
||||
for _, k := range crdKinds {
|
||||
unstruct, err := c.Get(k, metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, errors.Wrapf(err, "error waiting for %s to be ready", k)
|
||||
}
|
||||
|
||||
crd := new(apiextv1beta1.CustomResourceDefinition)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstruct.Object, crd); err != nil {
|
||||
return false, errors.Wrapf(err, "error converting %s from unstructured", k)
|
||||
}
|
||||
|
||||
foundCRDs = append(foundCRDs, crd)
|
||||
}
|
||||
|
||||
if len(foundCRDs) != len(crdKinds) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, crd := range foundCRDs {
|
||||
if !crdIsReady(crd) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
}
|
||||
areReady = true
|
||||
|
||||
return true, nil
|
||||
})
|
||||
return areReady, nil
|
||||
}
|
||||
|
||||
func isAvailable(c appsv1beta1.DeploymentCondition) bool {
|
||||
// Make sure that the deployment has been available for at least 10 seconds.
|
||||
// This is because the deployment can show as Ready momentarily before the pods fall into a CrashLoopBackOff.
|
||||
// See podutils.IsPodAvailable upstream for similar logic with pods
|
||||
if c.Type == appsv1beta1.DeploymentAvailable && c.Status == corev1.ConditionTrue {
|
||||
if !c.LastTransitionTime.IsZero() && c.LastTransitionTime.Add(10*time.Second).Before(time.Now()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DeploymentIsReady will poll the kubernetes API server to see if the velero deployment is ready to service user requests.
|
||||
func DeploymentIsReady(factory client.DynamicFactory) (bool, error) {
|
||||
gvk := schema.FromAPIVersionAndKind(appsv1beta1.SchemeGroupVersion.String(), "Deployment")
|
||||
apiResource := metav1.APIResource{
|
||||
Name: "deployments",
|
||||
Namespaced: true,
|
||||
}
|
||||
c, err := factory.ClientForGroupVersionResource(gvk.GroupVersion(), apiResource, "velero")
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Error creating client for deployment polling")
|
||||
}
|
||||
// declare this variable out of scope so we can return it
|
||||
var isReady bool
|
||||
var readyObservations int32
|
||||
err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) {
|
||||
unstructuredDeployment, err := c.Get("velero", metav1.GetOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, errors.Wrap(err, "error waiting for deployment to be ready")
|
||||
}
|
||||
|
||||
deploy := new(appsv1beta1.Deployment)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredDeployment.Object, deploy); err != nil {
|
||||
return false, errors.Wrap(err, "error converting deployment from unstructured")
|
||||
}
|
||||
|
||||
for _, cond := range deploy.Status.Conditions {
|
||||
if isAvailable(cond) {
|
||||
readyObservations++
|
||||
}
|
||||
}
|
||||
// Make sure we query the deployment enough times to see the state change, provided there is one.
|
||||
if readyObservations > 4 {
|
||||
isReady = true
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
})
|
||||
return isReady, err
|
||||
}
|
||||
|
||||
// GroupResources groups resources based on whether the resources are CustomResourceDefinitions or other types of kubernetes objects
|
||||
// This is useful to wait for readiness before creating CRD objects
|
||||
func GroupResources(resources *unstructured.UnstructuredList) *ResourceGroup {
|
||||
rg := new(ResourceGroup)
|
||||
|
||||
for i, r := range resources.Items {
|
||||
if r.GetKind() == "CustomResourceDefinition" {
|
||||
rg.CRDResources = append(rg.CRDResources, &resources.Items[i])
|
||||
continue
|
||||
}
|
||||
rg.OtherResources = append(rg.OtherResources, &resources.Items[i])
|
||||
}
|
||||
|
||||
return rg
|
||||
}
|
||||
|
||||
// createResource attempts to create a resource in the cluster.
|
||||
// If the resource already exists in the cluster, it's merely logged.
|
||||
func createResource(r *unstructured.Unstructured, factory client.DynamicFactory, w io.Writer) error {
|
||||
id := fmt.Sprintf("%s/%s", r.GetKind(), r.GetName())
|
||||
|
||||
// Helper to reduce boilerplate message about the same object
|
||||
log := func(f string, a ...interface{}) {
|
||||
format := strings.Join([]string{id, ": ", f, "\n"}, "")
|
||||
fmt.Fprintf(w, format, a...)
|
||||
}
|
||||
log("attempting to create resource")
|
||||
|
||||
gvk := schema.FromAPIVersionAndKind(r.GetAPIVersion(), r.GetKind())
|
||||
|
||||
apiResource := metav1.APIResource{
|
||||
Name: kindToResource[r.GetKind()],
|
||||
Namespaced: (r.GetNamespace() != ""),
|
||||
}
|
||||
|
||||
c, err := factory.ClientForGroupVersionResource(gvk.GroupVersion(), apiResource, r.GetNamespace())
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Error creating client for resource %s", id)
|
||||
}
|
||||
|
||||
if _, err := c.Create(r); apierrors.IsAlreadyExists(err) {
|
||||
log("already exists, proceeding")
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "Error creating resource %s", id)
|
||||
}
|
||||
|
||||
log("created")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Install creates resources on the Kubernetes cluster.
|
||||
// An unstructured list of resources is sent, one at a time, to the server. These are assumed to be in the preferred order already.
|
||||
// Resources will be sorted into CustomResourceDefinitions and any other resource type, and the function will wait up to 1 minute
|
||||
// for CRDs to be ready before proceeding.
|
||||
// An io.Writer can be used to output to a log or the console.
|
||||
func Install(factory client.DynamicFactory, resources *unstructured.UnstructuredList, w io.Writer) error {
|
||||
rg := GroupResources(resources)
|
||||
|
||||
//Install CRDs first
|
||||
for _, r := range rg.CRDResources {
|
||||
if err := createResource(r, factory, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for CRDs to be ready before proceeding
|
||||
fmt.Fprint(w, "Waiting for resources to be ready in cluster...\n")
|
||||
_, err := crdsAreReady(factory, []string{"backupstoragelocations.velero.io", "volumesnapshotlocations.velero.io"})
|
||||
if err == wait.ErrWaitTimeout {
|
||||
return errors.Errorf("timeout reached, CRDs not ready")
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Install all other resources
|
||||
for _, r := range rg.OtherResources {
|
||||
if err = createResource(r, factory, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -20,10 +20,17 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
"github.com/heptio/velero/pkg/buildinfo"
|
||||
)
|
||||
|
||||
// DefaultImage is the default image to use for the Velero deployment and restic daemonset containers.
|
||||
var DefaultImage = "gcr.io/heptio-images/velero:" + buildinfo.Version
|
||||
|
||||
func labels() map[string]string {
|
||||
return map[string]string{
|
||||
"component": "velero",
|
||||
@@ -132,3 +139,92 @@ func VolumeSnapshotLocation(namespace, provider string, config map[string]string
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Secret(namespace string, data []byte) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
ObjectMeta: objectMeta(namespace, "cloud-credentials"),
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"cloud": data,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
}
|
||||
}
|
||||
|
||||
func appendUnstructured(list *unstructured.UnstructuredList, obj runtime.Object) error {
|
||||
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
|
||||
// Remove the status field so we're not sending blank data to the server.
|
||||
// On CRDs, having an empty status is actually a validation error.
|
||||
delete(u, "status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list.Items = append(list.Items, unstructured.Unstructured{Object: u})
|
||||
return nil
|
||||
}
|
||||
|
||||
type VeleroOptions struct {
|
||||
Namespace string
|
||||
Image string
|
||||
ProviderName string
|
||||
Bucket string
|
||||
Prefix string
|
||||
SecretData []byte
|
||||
RestoreOnly bool
|
||||
UseRestic bool
|
||||
BSLConfig map[string]string
|
||||
VSLConfig map[string]string
|
||||
}
|
||||
|
||||
// AllResources returns a list of all resources necessary to install Velero, in the appropriate order, into a Kubernetes cluster.
|
||||
// Items are unstructured, since there are different data types returned.
|
||||
func AllResources(o *VeleroOptions) (*unstructured.UnstructuredList, error) {
|
||||
resources := new(unstructured.UnstructuredList)
|
||||
// Set the GVK so that the serialization framework outputs the list properly
|
||||
resources.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "List"})
|
||||
|
||||
for _, crd := range CRDs() {
|
||||
appendUnstructured(resources, crd)
|
||||
}
|
||||
|
||||
ns := Namespace(o.Namespace)
|
||||
appendUnstructured(resources, ns)
|
||||
|
||||
crb := ClusterRoleBinding(o.Namespace)
|
||||
appendUnstructured(resources, crb)
|
||||
|
||||
sa := ServiceAccount(o.Namespace)
|
||||
appendUnstructured(resources, sa)
|
||||
|
||||
sec := Secret(o.Namespace, o.SecretData)
|
||||
appendUnstructured(resources, sec)
|
||||
|
||||
bsl := BackupStorageLocation(o.Namespace, o.ProviderName, o.Bucket, o.Prefix, o.BSLConfig)
|
||||
appendUnstructured(resources, bsl)
|
||||
|
||||
vsl := VolumeSnapshotLocation(o.Namespace, o.ProviderName, o.VSLConfig)
|
||||
appendUnstructured(resources, vsl)
|
||||
|
||||
deploy := Deployment(o.Namespace,
|
||||
WithImage(o.Image),
|
||||
)
|
||||
if o.RestoreOnly {
|
||||
deploy = Deployment(o.Namespace,
|
||||
WithImage(o.Image),
|
||||
WithRestoreOnly(),
|
||||
)
|
||||
}
|
||||
appendUnstructured(resources, deploy)
|
||||
|
||||
if o.UseRestic {
|
||||
ds := DaemonSet(o.Namespace,
|
||||
WithImage(o.Image),
|
||||
)
|
||||
appendUnstructured(resources, ds)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func Encode(obj runtime.Object, format string) ([]byte, error) {
|
||||
// EncodeTo converts the provided object to the specified format and
|
||||
// writes the encoded data to the provided io.Writer.
|
||||
func EncodeTo(obj runtime.Object, format string, w io.Writer) error {
|
||||
encoder, err := EncoderFor(format)
|
||||
encoder, err := EncoderFor(format, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,7 +51,8 @@ func EncodeTo(obj runtime.Object, format string, w io.Writer) error {
|
||||
}
|
||||
|
||||
// EncoderFor gets the appropriate encoder for the specified format.
|
||||
func EncoderFor(format string) (runtime.Encoder, error) {
|
||||
// Only objects registered in the velero scheme, or objects with their TypeMeta set will have valid encoders.
|
||||
func EncoderFor(format string, obj runtime.Object) (runtime.Encoder, error) {
|
||||
var encoder runtime.Encoder
|
||||
desiredMediaType := fmt.Sprintf("application/%s", format)
|
||||
serializerInfo, found := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), desiredMediaType)
|
||||
@@ -63,6 +64,9 @@ func EncoderFor(format string) (runtime.Encoder, error) {
|
||||
} else {
|
||||
encoder = serializerInfo.Serializer
|
||||
}
|
||||
if !obj.GetObjectKind().GroupVersionKind().Empty() {
|
||||
return encoder, nil
|
||||
}
|
||||
encoder = scheme.Codecs.EncoderForVersion(encoder, v1.SchemeGroupVersion)
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user