github.com/pachyderm/pachyderm@v1.13.4/src/server/pkg/deploy/cmds/cmds.go (about)

     1  package cmds
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"math/rand"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"regexp"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/pachyderm/pachyderm/src/client"
    22  	"github.com/pachyderm/pachyderm/src/client/auth"
    23  	"github.com/pachyderm/pachyderm/src/client/enterprise"
    24  	"github.com/pachyderm/pachyderm/src/client/pkg/config"
    25  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    26  	"github.com/pachyderm/pachyderm/src/client/pkg/grpcutil"
    27  	"github.com/pachyderm/pachyderm/src/client/pkg/helm"
    28  	"github.com/pachyderm/pachyderm/src/client/version"
    29  	"github.com/pachyderm/pachyderm/src/server/pkg/cmdutil"
    30  	"github.com/pachyderm/pachyderm/src/server/pkg/deploy"
    31  	"github.com/pachyderm/pachyderm/src/server/pkg/deploy/assets"
    32  	"github.com/pachyderm/pachyderm/src/server/pkg/deploy/images"
    33  	_metrics "github.com/pachyderm/pachyderm/src/server/pkg/metrics"
    34  	"github.com/pachyderm/pachyderm/src/server/pkg/obj"
    35  	"github.com/pachyderm/pachyderm/src/server/pkg/serde"
    36  	clientcmd "k8s.io/client-go/tools/clientcmd/api/v1"
    37  
    38  	docker "github.com/fsouza/go-dockerclient"
    39  	log "github.com/sirupsen/logrus"
    40  	"github.com/spf13/cobra"
    41  )
    42  
    43  var (
    44  	awsAccessKeyIDRE = regexp.MustCompile("^[A-Z0-9]{20}$")
    45  	awsSecretRE      = regexp.MustCompile("^[A-Za-z0-9/+=]{40}$")
    46  	awsRegionRE      = regexp.MustCompile("^[a-z]{2}(?:-gov)?-[a-z]+-[0-9]$")
    47  )
    48  
    49  const (
    50  	defaultPachdShards = 16
    51  
    52  	defaultDashImage   = "pachyderm/dash"
    53  	defaultDashVersion = "0.5.57"
    54  
    55  	defaultIDEHubImage  = "pachyderm/ide-hub"
    56  	defaultIDEUserImage = "pachyderm/ide-user"
    57  
    58  	defaultIDEVersion      = "1.1.0"
    59  	defaultIDEChartVersion = "0.9.1" // see https://jupyterhub.github.io/helm-chart/
    60  
    61  	ideNotes = `
    62  Thanks for installing the Pachyderm IDE!
    63  
    64  It may take a few minutes for all of the pods to spin up. If you have kubectl
    65  access, you can check progress with:
    66  
    67    kubectl get pod -l release=pachyderm-ide
    68  
    69  Once all of the pods are in the 'Ready' status, you can access the IDE in the
    70  following manners:
    71  
    72  * If you're on docker for mac, it should be accessible on 'localhost'.
    73  * If you're on minikube, run 'minikube service proxy-public --url' -- one or
    74    both of the URLs printed should reach the IDE.
    75  * If you're on a cloud deployment, use the external IP of
    76    'kubectl get service proxy-public'.
    77  
    78  For more information about the Pachyderm IDE, see these resources:
    79  
    80  * Our how-tos: https://docs.pachyderm.com/latest/how-tos/use-pachyderm-ide/
    81  * The Z2JH docs, which the IDE builds off of:
    82    https://zero-to-jupyterhub.readthedocs.io/en/latest/
    83  `
    84  )
    85  
    86  func kubectl(stdin io.Reader, context *config.Context, args ...string) error {
    87  	var environ []string = nil
    88  	if context != nil {
    89  		tmpfile, err := ioutil.TempFile("", "transient-kube-config-*.yaml")
    90  		if err != nil {
    91  			return errors.Wrapf(err, "failed to create transient kube config")
    92  		}
    93  		defer os.Remove(tmpfile.Name())
    94  
    95  		config := clientcmd.Config{
    96  			Kind:           "Config",
    97  			APIVersion:     "v1",
    98  			CurrentContext: "pachyderm-active-context",
    99  			Contexts: []clientcmd.NamedContext{
   100  				clientcmd.NamedContext{
   101  					Name: "pachyderm-active-context",
   102  					Context: clientcmd.Context{
   103  						Cluster:   context.ClusterName,
   104  						AuthInfo:  context.AuthInfo,
   105  						Namespace: context.Namespace,
   106  					},
   107  				},
   108  			},
   109  		}
   110  
   111  		var buf bytes.Buffer
   112  		if err := encoder("yaml", &buf).Encode(config); err != nil {
   113  			return errors.Wrapf(err, "failed to encode config")
   114  		}
   115  
   116  		tmpfile.Write(buf.Bytes())
   117  		tmpfile.Close()
   118  
   119  		kubeconfig := os.Getenv("KUBECONFIG")
   120  		if kubeconfig == "" {
   121  			home, err := os.UserHomeDir()
   122  			if err != nil {
   123  				return errors.Wrapf(err, "failed to discover default kube config: could not get user home directory")
   124  			}
   125  			kubeconfig = path.Join(home, ".kube", "config")
   126  			if _, err = os.Stat(kubeconfig); errors.Is(err, os.ErrNotExist) {
   127  				return errors.Wrapf(err, "failed to discover default kube config: %q does not exist", kubeconfig)
   128  			}
   129  		}
   130  		kubeconfig = fmt.Sprintf("%s%c%s", kubeconfig, os.PathListSeparator, tmpfile.Name())
   131  
   132  		// note that this will override `KUBECONFIG` (if it is already defined) in
   133  		// the environment; see examples under
   134  		// https://golang.org/pkg/os/exec/#Command
   135  		environ = os.Environ()
   136  		environ = append(environ, fmt.Sprintf("KUBECONFIG=%s", kubeconfig))
   137  
   138  		if stdin == nil {
   139  			stdin = os.Stdin
   140  		}
   141  	}
   142  
   143  	ioObj := cmdutil.IO{
   144  		Stdin:   stdin,
   145  		Stdout:  os.Stdout,
   146  		Stderr:  os.Stderr,
   147  		Environ: environ,
   148  	}
   149  
   150  	args = append([]string{"kubectl"}, args...)
   151  	return cmdutil.RunIO(ioObj, args...)
   152  }
   153  
   154  // Generates a random secure token, in hex
   155  func generateSecureToken(length int) string {
   156  	b := make([]byte, length)
   157  	if _, err := rand.Read(b); err != nil {
   158  		return ""
   159  	}
   160  	return hex.EncodeToString(b)
   161  }
   162  
   163  // Return the appropriate encoder for the given output format.
   164  func encoder(output string, w io.Writer) serde.Encoder {
   165  	if output == "" {
   166  		output = "json"
   167  	} else {
   168  		output = strings.ToLower(output)
   169  	}
   170  	e, err := serde.GetEncoder(output, w,
   171  		serde.WithIndent(2),
   172  		serde.WithOrigName(true),
   173  	)
   174  	if err != nil {
   175  		cmdutil.ErrorAndExit(err.Error())
   176  	}
   177  	return e
   178  }
   179  
   180  func kubectlCreate(dryRun bool, manifest []byte, opts *assets.AssetOpts) error {
   181  	if dryRun {
   182  		_, err := os.Stdout.Write(manifest)
   183  		return err
   184  	}
   185  	// we set --validate=false due to https://github.com/kubernetes/kubernetes/issues/53309
   186  	if err := kubectl(bytes.NewReader(manifest), nil, "apply", "-f", "-", "--validate=false", "--namespace", opts.Namespace); err != nil {
   187  		return err
   188  	}
   189  
   190  	fmt.Println("\nPachyderm is launching. Check its status with \"kubectl get all\"")
   191  	if opts.DashOnly || !opts.NoDash {
   192  		fmt.Println("Once launched, access the dashboard by running \"pachctl port-forward\"")
   193  	}
   194  	fmt.Println("")
   195  
   196  	return nil
   197  }
   198  
   199  // findEquivalentContext searches for a context in the existing config that
   200  // references the same cluster as the context passed in. If no such context
   201  // was found, default values are returned instead.
   202  func findEquivalentContext(cfg *config.Config, to *config.Context) (string, *config.Context) {
   203  	// first check the active context
   204  	activeContextName, activeContext, _ := cfg.ActiveContext(false)
   205  	if activeContextName != "" && to.EqualClusterReference(activeContext) {
   206  		return activeContextName, activeContext
   207  	}
   208  
   209  	// failing that, search all contexts (sorted by name to be deterministic)
   210  	contextNames := []string{}
   211  	for contextName := range cfg.V2.Contexts {
   212  		contextNames = append(contextNames, contextName)
   213  	}
   214  	sort.Strings(contextNames)
   215  	for _, contextName := range contextNames {
   216  		existingContext := cfg.V2.Contexts[contextName]
   217  
   218  		if to.EqualClusterReference(existingContext) {
   219  			return contextName, existingContext
   220  		}
   221  	}
   222  
   223  	return "", nil
   224  }
   225  
   226  func contextCreate(namePrefix, namespace, serverCert string) error {
   227  	kubeConfig, err := config.RawKubeConfig()
   228  	if err != nil {
   229  		return err
   230  	}
   231  	kubeContext := kubeConfig.Contexts[kubeConfig.CurrentContext]
   232  
   233  	clusterName := ""
   234  	authInfo := ""
   235  	if kubeContext != nil {
   236  		clusterName = kubeContext.Cluster
   237  		authInfo = kubeContext.AuthInfo
   238  	}
   239  
   240  	cfg, err := config.Read(false, false)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	newContext := &config.Context{
   246  		Source:      config.ContextSource_IMPORTED,
   247  		ClusterName: clusterName,
   248  		AuthInfo:    authInfo,
   249  		Namespace:   namespace,
   250  		ServerCAs:   serverCert,
   251  	}
   252  
   253  	equivalentContextName, equivalentContext := findEquivalentContext(cfg, newContext)
   254  	if equivalentContext != nil {
   255  		cfg.V2.ActiveContext = equivalentContextName
   256  		equivalentContext.Source = newContext.Source
   257  		equivalentContext.ClusterDeploymentID = ""
   258  		equivalentContext.ServerCAs = newContext.ServerCAs
   259  		return cfg.Write()
   260  	}
   261  
   262  	// we couldn't find an existing context that is the same as the new one,
   263  	// so we'll have to create it
   264  	newContextName := namePrefix
   265  	if _, ok := cfg.V2.Contexts[newContextName]; ok {
   266  		newContextName = fmt.Sprintf("%s-%s", namePrefix, time.Now().Format("2006-01-02-15-04-05"))
   267  	}
   268  
   269  	cfg.V2.Contexts[newContextName] = newContext
   270  	cfg.V2.ActiveContext = newContextName
   271  	return cfg.Write()
   272  }
   273  
   274  // containsEmpty is a helper function used for validation (particularly for
   275  // validating that creds arguments aren't empty
   276  func containsEmpty(vals []string) bool {
   277  	for _, val := range vals {
   278  		if val == "" {
   279  			return true
   280  		}
   281  	}
   282  	return false
   283  }
   284  
   285  // deprecationWarning prints a deprecation warning to os.Stderr.
   286  func deprecationWarning(msg string) {
   287  	fmt.Fprintf(os.Stderr, "DEPRECATED: %s\n\n", msg)
   288  }
   289  
   290  func getKubeNamespace() string {
   291  	kubeConfig := config.KubeConfig(nil)
   292  	var err error
   293  	namespace, _, err := kubeConfig.Namespace()
   294  	if err != nil {
   295  		log.Warningf("using namespace \"default\" (couldn't load namespace "+
   296  			"from kubernetes config: %v)\n", err)
   297  		namespace = "default"
   298  	}
   299  	return namespace
   300  }
   301  
   302  func standardDeployCmds() []*cobra.Command {
   303  	var commands []*cobra.Command
   304  	var opts *assets.AssetOpts
   305  
   306  	var dryRun bool
   307  	var outputFormat string
   308  	var namespace string
   309  	var serverCert string
   310  	var blockCacheSize string
   311  	var dashImage string
   312  	var dashOnly bool
   313  	var etcdCPURequest string
   314  	var etcdMemRequest string
   315  	var etcdNodes int
   316  	var etcdStorageClassName string
   317  	var etcdVolume string
   318  	var exposeObjectAPI bool
   319  	var imagePullSecret string
   320  	var localRoles bool
   321  	var logLevel string
   322  	var storageV2 bool
   323  	var noDash bool
   324  	var noExposeDockerSocket bool
   325  	var noGuaranteed bool
   326  	var noRBAC bool
   327  	var pachdCPURequest string
   328  	var pachdNonCacheMemRequest string
   329  	var pachdShards int
   330  	var registry string
   331  	var tlsCertKey string
   332  	var uploadConcurrencyLimit int
   333  	var putFileConcurrencyLimit int
   334  	var clusterDeploymentID string
   335  	var requireCriticalServersOnly bool
   336  	var workerServiceAccountName string
   337  	appendGlobalFlags := func(cmd *cobra.Command) {
   338  		cmd.Flags().IntVar(&pachdShards, "shards", defaultPachdShards, "(rarely set) The maximum number of pachd nodes allowed in the cluster; increasing this number blindly can result in degraded performance.")
   339  		cmd.Flags().IntVar(&etcdNodes, "dynamic-etcd-nodes", 0, "Deploy etcd as a StatefulSet with the given number of pods.  The persistent volumes used by these pods are provisioned dynamically.  Note that StatefulSet is currently a beta kubernetes feature, which might be unavailable in older versions of kubernetes.")
   340  		cmd.Flags().StringVar(&etcdVolume, "static-etcd-volume", "", "Deploy etcd as a ReplicationController with one pod.  The pod uses the given persistent volume.")
   341  		cmd.Flags().StringVar(&etcdStorageClassName, "etcd-storage-class", "", "If set, the name of an existing StorageClass to use for etcd storage. Ignored if --static-etcd-volume is set.")
   342  		cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Don't actually deploy pachyderm to Kubernetes, instead just print the manifest. Note that a pachyderm context will not be created, unless you also use `--create-context`.")
   343  		cmd.Flags().StringVarP(&outputFormat, "output", "o", "json", "Output format. One of: json|yaml")
   344  		cmd.Flags().StringVar(&logLevel, "log-level", "info", "The level of log messages to print options are, from least to most verbose: \"error\", \"info\", \"debug\".")
   345  		cmd.Flags().BoolVar(&dashOnly, "dashboard-only", false, "Only deploy the Pachyderm UI (experimental), without the rest of pachyderm. This is for launching the UI adjacent to an existing Pachyderm cluster. After deployment, run \"pachctl port-forward\" to connect")
   346  		cmd.Flags().BoolVar(&noDash, "no-dashboard", false, "Don't deploy the Pachyderm UI alongside Pachyderm (experimental).")
   347  		cmd.Flags().StringVar(&registry, "registry", "", "The registry to pull images from.")
   348  		cmd.Flags().StringVar(&imagePullSecret, "image-pull-secret", "", "A secret in Kubernetes that's needed to pull from your private registry.")
   349  		cmd.Flags().StringVar(&dashImage, "dash-image", "", "Image URL for pachyderm dashboard")
   350  		cmd.Flags().BoolVar(&noGuaranteed, "no-guaranteed", false, "Don't use guaranteed QoS for etcd and pachd deployments. Turning this on (turning guaranteed QoS off) can lead to more stable local clusters (such as on Minikube), it should normally be used for production clusters.")
   351  		cmd.Flags().BoolVar(&noRBAC, "no-rbac", false, "Don't deploy RBAC roles for Pachyderm. (for k8s versions prior to 1.8)")
   352  		cmd.Flags().BoolVar(&localRoles, "local-roles", false, "Use namespace-local roles instead of cluster roles. Ignored if --no-rbac is set.")
   353  		cmd.Flags().StringVar(&namespace, "namespace", "", "Kubernetes namespace to deploy Pachyderm to.")
   354  		cmd.Flags().BoolVar(&noExposeDockerSocket, "no-expose-docker-socket", false, "Don't expose the Docker socket to worker containers. This limits the privileges of workers which prevents them from automatically setting the container's working dir and user.")
   355  		cmd.Flags().BoolVar(&exposeObjectAPI, "expose-object-api", false, "If set, instruct pachd to serve its object/block API on its public port (not safe with auth enabled, do not set in production).")
   356  		cmd.Flags().StringVar(&tlsCertKey, "tls", "", "string of the form \"<cert path>,<key path>\" of the signed TLS certificate and private key that Pachd should use for TLS authentication (enables TLS-encrypted communication with Pachd)")
   357  		cmd.Flags().BoolVar(&storageV2, "storage-v2", false, "Deploy Pachyderm using V2 storage (alpha)")
   358  		cmd.Flags().IntVar(&uploadConcurrencyLimit, "upload-concurrency-limit", assets.DefaultUploadConcurrencyLimit, "The maximum number of concurrent object storage uploads per Pachd instance.")
   359  		cmd.Flags().IntVar(&putFileConcurrencyLimit, "put-file-concurrency-limit", assets.DefaultPutFileConcurrencyLimit, "The maximum number of files to upload or fetch from remote sources (HTTP, blob storage) using PutFile concurrently.")
   360  		cmd.Flags().StringVar(&clusterDeploymentID, "cluster-deployment-id", "", "Set an ID for the cluster deployment. Defaults to a random value.")
   361  		cmd.Flags().BoolVar(&requireCriticalServersOnly, "require-critical-servers-only", assets.DefaultRequireCriticalServersOnly, "Only require the critical Pachd servers to startup and run without errors.")
   362  		cmd.Flags().StringVar(&workerServiceAccountName, "worker-service-account", assets.DefaultWorkerServiceAccountName, "The Kubernetes service account for workers to use when creating S3 gateways.")
   363  
   364  		// Flags for setting pachd resource requests. These should rarely be set --
   365  		// only if we get the defaults wrong, or users have an unusual access pattern
   366  		//
   367  		// All of these are empty by default, because the actual default values depend
   368  		// on the backend to which we're. The defaults are set in
   369  		// s/s/pkg/deploy/assets/assets.go
   370  		cmd.Flags().StringVar(&pachdCPURequest,
   371  			"pachd-cpu-request", "", "(rarely set) The size of Pachd's CPU "+
   372  				"request, which we give to Kubernetes. Size is in cores (with partial "+
   373  				"cores allowed and encouraged).")
   374  		cmd.Flags().StringVar(&blockCacheSize, "block-cache-size", "",
   375  			"Size of pachd's in-memory cache for PFS files. Size is specified in "+
   376  				"bytes, with allowed SI suffixes (M, K, G, Mi, Ki, Gi, etc).")
   377  		cmd.Flags().StringVar(&pachdNonCacheMemRequest,
   378  			"pachd-memory-request", "", "(rarely set) The size of PachD's memory "+
   379  				"request in addition to its block cache (set via --block-cache-size). "+
   380  				"Size is in bytes, with SI suffixes (M, K, G, Mi, Ki, Gi, etc).")
   381  		cmd.Flags().StringVar(&etcdCPURequest,
   382  			"etcd-cpu-request", "", "(rarely set) The size of etcd's CPU request, "+
   383  				"which we give to Kubernetes. Size is in cores (with partial cores "+
   384  				"allowed and encouraged).")
   385  		cmd.Flags().StringVar(&etcdMemRequest,
   386  			"etcd-memory-request", "", "(rarely set) The size of etcd's memory "+
   387  				"request. Size is in bytes, with SI suffixes (M, K, G, Mi, Ki, Gi, "+
   388  				"etc).")
   389  	}
   390  	checkDeprecatedGlobalFlags := func() {
   391  		if dashImage != "" {
   392  			deprecationWarning("The dash-image flag will be removed in a future version.  To specify a particular dash image, consider using the pachyderm/pachyderm Helm chart.")
   393  		}
   394  		if dashOnly {
   395  			deprecationWarning("The dash-only flag will be removed in a future version.")
   396  		}
   397  		if noDash {
   398  			deprecationWarning("The no-dashboard flag will be removed in a future version.")
   399  		}
   400  		if exposeObjectAPI {
   401  			deprecationWarning("The expose-object-api flag will be removed in a future version.")
   402  		}
   403  		if storageV2 {
   404  			deprecationWarning("The storage-v2 flag will be removed in a future version.")
   405  		}
   406  		if pachdShards != defaultPachdShards {
   407  			deprecationWarning("The shards flag will be removed in a future version.  To specify the number of shards, consider using the pachyderm/pachyderm Helm chart.")
   408  		}
   409  		if noRBAC {
   410  			deprecationWarning("The no-rbac flag will be removed in a future version.  To prevent creation of RBAC objects, consider using the pachyderm/pachyderm Helm chart.")
   411  		}
   412  		if noGuaranteed {
   413  			deprecationWarning("The no-guaranteed flag will be removed in a future version.  To remove resource limits, consider using the pachyderm/pachyderm Helm chart.")
   414  		}
   415  		if etcdVolume != "" {
   416  			deprecationWarning("Specification of a static etcd volume will be removed in a future version.")
   417  		}
   418  	}
   419  
   420  	var retries int
   421  	var timeout string
   422  	var uploadACL string
   423  	var reverse bool
   424  	var partSize int64
   425  	var maxUploadParts int
   426  	var disableSSL bool
   427  	var noVerifySSL bool
   428  	var logOptions string
   429  	appendS3Flags := func(cmd *cobra.Command) {
   430  		cmd.Flags().IntVar(&retries, "retries", obj.DefaultRetries, "(rarely set) Set a custom number of retries for object storage requests.")
   431  		cmd.Flags().StringVar(&timeout, "timeout", obj.DefaultTimeout, "(rarely set) Set a custom timeout for object storage requests.")
   432  		cmd.Flags().StringVar(&uploadACL, "upload-acl", obj.DefaultUploadACL, "(rarely set) Set a custom upload ACL for object storage uploads.")
   433  		cmd.Flags().BoolVar(&reverse, "reverse", obj.DefaultReverse, "(rarely set) Reverse object storage paths.")
   434  		cmd.Flags().Int64Var(&partSize, "part-size", obj.DefaultPartSize, "(rarely set) Set a custom part size for object storage uploads.")
   435  		cmd.Flags().IntVar(&maxUploadParts, "max-upload-parts", obj.DefaultMaxUploadParts, "(rarely set) Set a custom maximum number of upload parts.")
   436  		cmd.Flags().BoolVar(&disableSSL, "disable-ssl", obj.DefaultDisableSSL, "(rarely set) Disable SSL.")
   437  		cmd.Flags().BoolVar(&noVerifySSL, "no-verify-ssl", obj.DefaultNoVerifySSL, "(rarely set) Skip SSL certificate verification (typically used for enabling self-signed certificates).")
   438  		cmd.Flags().StringVar(&logOptions, "obj-log-options", obj.DefaultAwsLogOptions, "(rarely set) Enable verbose logging in Pachyderm's internal S3 client for debugging. Comma-separated list containing zero or more of: 'Debug', 'Signing', 'HTTPBody', 'RequestRetries', 'RequestErrors', 'EventStreamBody', or 'all' (case-insensitive). See 'AWS SDK for Go' docs for details.")
   439  	}
   440  	checkS3Flags := func() {
   441  		if disableSSL != obj.DefaultDisableSSL {
   442  			deprecationWarning("The disable-ssl flag will be removed in a future version.  To disable SSL, consider using the pachyderm/pachyderm Helm chart.")
   443  		}
   444  		if maxUploadParts != obj.DefaultMaxUploadParts {
   445  			deprecationWarning("The max-upload-parts flag will be removed in a future version.  To specify the maximum number of upload parts, consider using the pachyderm/pachyderm Helm chart.")
   446  		}
   447  		if noVerifySSL != obj.DefaultNoVerifySSL {
   448  			deprecationWarning("The no-verify-ssl flag will be removed in a future version.  To disable SSL verification, consider using the pachyderm/pachyderm Helm chart.")
   449  		}
   450  		if logOptions != obj.DefaultAwsLogOptions {
   451  			deprecationWarning("The obj-log-options flag will be removed in a future version.  To specify S3 logging options, consider using the pachyderm/pachyderm Helm chart.")
   452  		}
   453  		if partSize != obj.DefaultPartSize {
   454  			deprecationWarning("The part-size flag will be removed in a future version.  To specify a custom part size for object uploads, consider using the pachyderm/pachyderm Helm chart.")
   455  		}
   456  		if retries != obj.DefaultRetries {
   457  			deprecationWarning("The retries flag will be removed in a future version.  To specify the number of retries for object storage requests, consider using the pachyderm/pachyderm Helm chart.")
   458  		}
   459  		if reverse != obj.DefaultReverse {
   460  			deprecationWarning("The reverse flag will be removed in a future version.  To specify whether to reverse object storage paths, consider using the pachyderm/pachyderm Helm chart.")
   461  		}
   462  		if timeout != obj.DefaultTimeout {
   463  			deprecationWarning("The timeout flag will be removed in a future version.  To specify an object storage request timeout, consider using the pachyderm/pachyderm Helm chart.")
   464  		}
   465  		if uploadACL != obj.DefaultUploadACL {
   466  			deprecationWarning("The upload-acl flag will be removed in a future version.  To specify an upload ACL, consider using the pachyderm/pachyderm Helm chart.")
   467  		}
   468  	}
   469  
   470  	var contextName string
   471  	var createContext bool
   472  	appendContextFlags := func(cmd *cobra.Command) {
   473  		cmd.Flags().StringVarP(&contextName, "context", "c", "", "Name of the context to add to the pachyderm config. If unspecified, a context name will automatically be derived.")
   474  		cmd.Flags().BoolVar(&createContext, "create-context", false, "Create a context, even with `--dry-run`.")
   475  	}
   476  
   477  	preRunInternal := func(args []string) error {
   478  		checkDeprecatedGlobalFlags()
   479  		cfg, err := config.Read(false, false)
   480  		if err != nil {
   481  			log.Warningf("could not read config to check whether cluster metrics "+
   482  				"will be enabled: %v.\n", err)
   483  		}
   484  
   485  		if namespace == "" {
   486  			namespace = getKubeNamespace()
   487  		}
   488  
   489  		if dashImage == "" {
   490  			dashImage = fmt.Sprintf("%s:%s", defaultDashImage, getCompatibleVersion("dash", "", defaultDashVersion))
   491  		}
   492  
   493  		opts = &assets.AssetOpts{
   494  			FeatureFlags: assets.FeatureFlags{
   495  				StorageV2: storageV2,
   496  			},
   497  			StorageOpts: assets.StorageOpts{
   498  				UploadConcurrencyLimit:  uploadConcurrencyLimit,
   499  				PutFileConcurrencyLimit: putFileConcurrencyLimit,
   500  			},
   501  			PachdShards:                uint64(pachdShards),
   502  			Version:                    version.PrettyPrintVersion(version.Version),
   503  			LogLevel:                   logLevel,
   504  			Metrics:                    cfg == nil || cfg.V2.Metrics,
   505  			PachdCPURequest:            pachdCPURequest,
   506  			PachdNonCacheMemRequest:    pachdNonCacheMemRequest,
   507  			BlockCacheSize:             blockCacheSize,
   508  			EtcdCPURequest:             etcdCPURequest,
   509  			EtcdMemRequest:             etcdMemRequest,
   510  			EtcdNodes:                  etcdNodes,
   511  			EtcdVolume:                 etcdVolume,
   512  			EtcdStorageClassName:       etcdStorageClassName,
   513  			DashOnly:                   dashOnly,
   514  			NoDash:                     noDash,
   515  			DashImage:                  dashImage,
   516  			Registry:                   registry,
   517  			ImagePullSecret:            imagePullSecret,
   518  			NoGuaranteed:               noGuaranteed,
   519  			NoRBAC:                     noRBAC,
   520  			LocalRoles:                 localRoles,
   521  			Namespace:                  namespace,
   522  			NoExposeDockerSocket:       noExposeDockerSocket,
   523  			ExposeObjectAPI:            exposeObjectAPI,
   524  			ClusterDeploymentID:        clusterDeploymentID,
   525  			RequireCriticalServersOnly: requireCriticalServersOnly,
   526  			WorkerServiceAccountName:   workerServiceAccountName,
   527  		}
   528  		if tlsCertKey != "" {
   529  			// TODO(msteffen): If either the cert path or the key path contains a
   530  			// comma, this doesn't work
   531  			certKey := strings.Split(tlsCertKey, ",")
   532  			if len(certKey) != 2 {
   533  				return fmt.Errorf("could not split TLS certificate and key correctly; must have two parts but got: %#v", certKey)
   534  			}
   535  			opts.TLS = &assets.TLSOpts{
   536  				ServerCert: certKey[0],
   537  				ServerKey:  certKey[1],
   538  			}
   539  
   540  			serverCertBytes, err := ioutil.ReadFile(certKey[0])
   541  			if err != nil {
   542  				return errors.Wrapf(err, "could not read server cert at %q", certKey[0])
   543  			}
   544  			serverCert = base64.StdEncoding.EncodeToString([]byte(serverCertBytes))
   545  		}
   546  		return nil
   547  	}
   548  	preRun := cmdutil.Run(preRunInternal)
   549  
   550  	deployPreRun := cmdutil.Run(func(args []string) error {
   551  		if version.IsUnstable() {
   552  			fmt.Fprintf(os.Stderr, "WARNING: The version of Pachyderm you are deploying (%s) is an unstable pre-release build and may not support data migration.\n\n", version.PrettyVersion())
   553  
   554  			if ok, err := cmdutil.InteractiveConfirm(); err != nil {
   555  				return err
   556  			} else if !ok {
   557  				return errors.New("deploy aborted")
   558  			}
   559  		}
   560  		return preRunInternal(args)
   561  	})
   562  
   563  	var dev bool
   564  	var hostPath string
   565  	deployLocal := &cobra.Command{
   566  		Short:  "Deploy a single-node Pachyderm cluster with local metadata storage.",
   567  		Long:   "Deploy a single-node Pachyderm cluster with local metadata storage.",
   568  		PreRun: deployPreRun,
   569  		Run: cmdutil.RunFixedArgs(0, func(args []string) (retErr error) {
   570  			if !dev {
   571  				start := time.Now()
   572  				startMetricsWait := _metrics.StartReportAndFlushUserAction("Deploy", start)
   573  				defer startMetricsWait()
   574  				defer func() {
   575  					finishMetricsWait := _metrics.FinishReportAndFlushUserAction("Deploy", retErr, start)
   576  					finishMetricsWait()
   577  				}()
   578  			}
   579  			if dev {
   580  				// Use dev build instead of release build
   581  				opts.Version = deploy.DevVersionTag
   582  
   583  				// we turn metrics off if this is a dev cluster. The default
   584  				// is set by deploy.PersistentPreRun, below.
   585  				opts.Metrics = false
   586  
   587  				// Disable authentication, for tests
   588  				opts.DisableAuthentication = true
   589  
   590  				// Serve the Pachyderm object/block API locally, as this is needed by
   591  				// our tests (and authentication is disabled anyway)
   592  				opts.ExposeObjectAPI = true
   593  			}
   594  			var buf bytes.Buffer
   595  			if err := assets.WriteLocalAssets(
   596  				encoder(outputFormat, &buf), opts, hostPath,
   597  			); err != nil {
   598  				return err
   599  			}
   600  			if err := kubectlCreate(dryRun, buf.Bytes(), opts); err != nil {
   601  				return err
   602  			}
   603  			if !dryRun || createContext {
   604  				if contextName == "" {
   605  					contextName = "local"
   606  				}
   607  				if err := contextCreate(contextName, namespace, serverCert); err != nil {
   608  					return err
   609  				}
   610  			}
   611  			return nil
   612  		}),
   613  	}
   614  	appendGlobalFlags(deployLocal)
   615  	appendContextFlags(deployLocal)
   616  	deployLocal.Flags().StringVar(&hostPath, "host-path", "/var/pachyderm", "Location on the host machine where PFS metadata will be stored.")
   617  	deployLocal.Flags().BoolVarP(&dev, "dev", "d", false, "Deploy pachd with local version tags, disable metrics, expose Pachyderm's object/block API, and use an insecure authentication mechanism (do not set on any cluster with sensitive data)")
   618  	commands = append(commands, cmdutil.CreateAlias(deployLocal, "deploy local"))
   619  
   620  	deployGoogle := &cobra.Command{
   621  		Use:   "{{alias}} <bucket-name> <disk-size> [<credentials-file>]",
   622  		Short: "Deploy a Pachyderm cluster running on Google Cloud Platform.",
   623  		Long: `Deploy a Pachyderm cluster running on Google Cloud Platform.
   624    <bucket-name>: A Google Cloud Storage bucket where Pachyderm will store PFS data.
   625    <disk-size>: Size of Google Compute Engine persistent disks in GB (assumed to all be the same).
   626    <credentials-file>: A file containing the private key for the account (downloaded from Google Compute Engine).`,
   627  		PreRun: deployPreRun,
   628  		Run: cmdutil.RunBoundedArgs(2, 3, func(args []string) (retErr error) {
   629  			start := time.Now()
   630  			startMetricsWait := _metrics.StartReportAndFlushUserAction("Deploy", start)
   631  			defer startMetricsWait()
   632  			defer func() {
   633  				finishMetricsWait := _metrics.FinishReportAndFlushUserAction("Deploy", retErr, start)
   634  				finishMetricsWait()
   635  			}()
   636  			volumeSize, err := strconv.Atoi(args[1])
   637  			if err != nil {
   638  				return errors.Errorf("volume size needs to be an integer; instead got %v", args[1])
   639  			}
   640  			var buf bytes.Buffer
   641  			opts.BlockCacheSize = "0G" // GCS is fast so we want to disable the block cache. See issue #1650
   642  			var cred string
   643  			if len(args) == 3 {
   644  				credBytes, err := ioutil.ReadFile(args[2])
   645  				if err != nil {
   646  					return errors.Wrapf(err, "error reading creds file %s", args[2])
   647  				}
   648  				cred = string(credBytes)
   649  			}
   650  			bucket := strings.TrimPrefix(args[0], "gs://")
   651  			if err = assets.WriteGoogleAssets(
   652  				encoder(outputFormat, &buf), opts, bucket, cred, volumeSize,
   653  			); err != nil {
   654  				return err
   655  			}
   656  			if err := kubectlCreate(dryRun, buf.Bytes(), opts); err != nil {
   657  				return err
   658  			}
   659  			if !dryRun || createContext {
   660  				if contextName == "" {
   661  					contextName = "gcs"
   662  				}
   663  				if err := contextCreate(contextName, namespace, serverCert); err != nil {
   664  					return err
   665  				}
   666  			}
   667  			return nil
   668  		}),
   669  	}
   670  	appendGlobalFlags(deployGoogle)
   671  	appendContextFlags(deployGoogle)
   672  	commands = append(commands, cmdutil.CreateAlias(deployGoogle, "deploy google"))
   673  	commands = append(commands, cmdutil.CreateAlias(deployGoogle, "deploy gcp"))
   674  
   675  	var objectStoreBackend string
   676  	var persistentDiskBackend string
   677  	var secure bool
   678  	var isS3V2 bool
   679  	deployCustom := &cobra.Command{
   680  		Use:   "{{alias}} --persistent-disk <persistent disk backend> --object-store <object store backend> <persistent disk args> <object store args>",
   681  		Short: "Deploy a custom Pachyderm cluster configuration",
   682  		Long: `Deploy a custom Pachyderm cluster configuration.
   683  If <object store backend> is \"s3\", then the arguments are:
   684      <volumes> <size of volumes (in GB)> <bucket> <id> <secret> <endpoint>`,
   685  		PreRun: deployPreRun,
   686  		Run: cmdutil.RunBoundedArgs(4, 7, func(args []string) (retErr error) {
   687  			checkS3Flags()
   688  			start := time.Now()
   689  			startMetricsWait := _metrics.StartReportAndFlushUserAction("Deploy", start)
   690  			defer startMetricsWait()
   691  			defer func() {
   692  				finishMetricsWait := _metrics.FinishReportAndFlushUserAction("Deploy", retErr, start)
   693  				finishMetricsWait()
   694  			}()
   695  			// Setup advanced configuration.
   696  			advancedConfig := &obj.AmazonAdvancedConfiguration{
   697  				Retries:        retries,
   698  				Timeout:        timeout,
   699  				UploadACL:      uploadACL,
   700  				Reverse:        reverse,
   701  				PartSize:       partSize,
   702  				MaxUploadParts: maxUploadParts,
   703  				DisableSSL:     disableSSL,
   704  				NoVerifySSL:    noVerifySSL,
   705  				LogOptions:     logOptions,
   706  			}
   707  			if isS3V2 {
   708  				fmt.Printf("DEPRECATED: Support for the S3V2 option is being deprecated. It will be removed in a future version\n\n")
   709  			}
   710  			// Generate manifest and write assets.
   711  			var buf bytes.Buffer
   712  			if err := assets.WriteCustomAssets(
   713  				encoder(outputFormat, &buf), opts, args, objectStoreBackend,
   714  				persistentDiskBackend, secure, isS3V2, advancedConfig,
   715  			); err != nil {
   716  				return err
   717  			}
   718  			if err := kubectlCreate(dryRun, buf.Bytes(), opts); err != nil {
   719  				return err
   720  			}
   721  			if !dryRun || createContext {
   722  				if contextName == "" {
   723  					contextName = "custom"
   724  				}
   725  				if err := contextCreate(contextName, namespace, serverCert); err != nil {
   726  					return err
   727  				}
   728  			}
   729  			return nil
   730  		}),
   731  	}
   732  	appendGlobalFlags(deployCustom)
   733  	appendS3Flags(deployCustom)
   734  	appendContextFlags(deployCustom)
   735  	// (bryce) secure should be merged with disableSSL, but it would be a breaking change.
   736  	deployCustom.Flags().BoolVarP(&secure, "secure", "s", false, "Enable secure access to a Minio server.")
   737  	deployCustom.Flags().StringVar(&persistentDiskBackend, "persistent-disk", "aws",
   738  		"(required) Backend providing persistent local volumes to stateful pods. "+
   739  			"One of: aws, google, or azure.")
   740  	deployCustom.Flags().StringVar(&objectStoreBackend, "object-store", "s3",
   741  		"(required) Backend providing an object-storage API to pachyderm. One of: "+
   742  			"s3, gcs, or azure-blob.")
   743  	deployCustom.Flags().BoolVar(&isS3V2, "isS3V2", false, "Enable S3V2 client (DEPRECATED)")
   744  	commands = append(commands, cmdutil.CreateAlias(deployCustom, "deploy custom"))
   745  
   746  	var cloudfrontDistribution string
   747  	var creds string
   748  	var iamRole string
   749  	var vault string
   750  	deployAmazon := &cobra.Command{
   751  		Use:   "{{alias}} <bucket-name> <region> <disk-size>",
   752  		Short: "Deploy a Pachyderm cluster running on AWS.",
   753  		Long: `Deploy a Pachyderm cluster running on AWS.
   754    <bucket-name>: An S3 bucket where Pachyderm will store PFS data.
   755    <region>: The AWS region where Pachyderm is being deployed (e.g. us-west-1)
   756    <disk-size>: Size of EBS volumes, in GB (assumed to all be the same).`,
   757  		PreRun: deployPreRun,
   758  		Run: cmdutil.RunFixedArgs(3, func(args []string) (retErr error) {
   759  			checkS3Flags()
   760  			if vault != "" {
   761  				deprecationWarning("The vault flag will be removed in a future version.")
   762  			}
   763  			start := time.Now()
   764  			startMetricsWait := _metrics.StartReportAndFlushUserAction("Deploy", start)
   765  			defer startMetricsWait()
   766  			defer func() {
   767  				finishMetricsWait := _metrics.FinishReportAndFlushUserAction("Deploy", retErr, start)
   768  				finishMetricsWait()
   769  			}()
   770  			if creds == "" && vault == "" && iamRole == "" {
   771  				return errors.Errorf("one of --credentials, --vault, or --iam-role needs to be provided")
   772  			}
   773  
   774  			// populate 'amazonCreds' & validate
   775  			var amazonCreds *assets.AmazonCreds
   776  			if creds != "" {
   777  				parts := strings.Split(creds, ",")
   778  				if len(parts) < 2 || len(parts) > 3 || containsEmpty(parts[:2]) {
   779  					return errors.Errorf("incorrect format of --credentials")
   780  				}
   781  				amazonCreds = &assets.AmazonCreds{ID: parts[0], Secret: parts[1]}
   782  				if len(parts) > 2 {
   783  					amazonCreds.Token = parts[2]
   784  				}
   785  
   786  				if !awsAccessKeyIDRE.MatchString(amazonCreds.ID) {
   787  					fmt.Fprintf(os.Stderr, "The AWS Access Key seems invalid (does not match %q)\n", awsAccessKeyIDRE)
   788  					if ok, err := cmdutil.InteractiveConfirm(); err != nil {
   789  						return err
   790  					} else if !ok {
   791  						return errors.Errorf("aborted")
   792  					}
   793  				}
   794  
   795  				if !awsSecretRE.MatchString(amazonCreds.Secret) {
   796  					fmt.Fprintf(os.Stderr, "The AWS Secret seems invalid (does not match %q)\n", awsSecretRE)
   797  					if ok, err := cmdutil.InteractiveConfirm(); err != nil {
   798  						return err
   799  					} else if !ok {
   800  						return errors.Errorf("aborted")
   801  					}
   802  				}
   803  			}
   804  			if vault != "" {
   805  				if amazonCreds != nil {
   806  					return errors.Errorf("only one of --credentials, --vault, or --iam-role needs to be provided")
   807  				}
   808  				parts := strings.Split(vault, ",")
   809  				if len(parts) != 3 || containsEmpty(parts) {
   810  					return errors.Errorf("incorrect format of --vault")
   811  				}
   812  				amazonCreds = &assets.AmazonCreds{VaultAddress: parts[0], VaultRole: parts[1], VaultToken: parts[2]}
   813  			}
   814  			if iamRole != "" {
   815  				if amazonCreds != nil {
   816  					return errors.Errorf("only one of --credentials, --vault, or --iam-role needs to be provided")
   817  				}
   818  				opts.IAMRole = iamRole
   819  			}
   820  			volumeSize, err := strconv.Atoi(args[2])
   821  			if err != nil {
   822  				return errors.Errorf("volume size needs to be an integer; instead got %v", args[2])
   823  			}
   824  			if strings.TrimSpace(cloudfrontDistribution) != "" {
   825  				log.Warningf("you specified a cloudfront distribution; deploying on " +
   826  					"AWS with cloudfront is currently an alpha feature. No security " +
   827  					"restrictions have been applied to cloudfront, making all data " +
   828  					"public (obscured but not secured)\n")
   829  			}
   830  			bucket, region := strings.TrimPrefix(args[0], "s3://"), args[1]
   831  			if !awsRegionRE.MatchString(region) {
   832  				fmt.Fprintf(os.Stderr, "The AWS region seems invalid (does not match %q)\n", awsRegionRE)
   833  				if ok, err := cmdutil.InteractiveConfirm(); err != nil {
   834  					return err
   835  				} else if !ok {
   836  					return errors.Errorf("aborted")
   837  				}
   838  			}
   839  			// Setup advanced configuration.
   840  			advancedConfig := &obj.AmazonAdvancedConfiguration{
   841  				Retries:        retries,
   842  				Timeout:        timeout,
   843  				UploadACL:      uploadACL,
   844  				Reverse:        reverse,
   845  				PartSize:       partSize,
   846  				MaxUploadParts: maxUploadParts,
   847  				DisableSSL:     disableSSL,
   848  				NoVerifySSL:    noVerifySSL,
   849  				LogOptions:     logOptions,
   850  			}
   851  			// Generate manifest and write assets.
   852  			var buf bytes.Buffer
   853  			if err = assets.WriteAmazonAssets(
   854  				encoder(outputFormat, &buf), opts, region, bucket, volumeSize,
   855  				amazonCreds, cloudfrontDistribution, advancedConfig,
   856  			); err != nil {
   857  				return err
   858  			}
   859  			if err := kubectlCreate(dryRun, buf.Bytes(), opts); err != nil {
   860  				return err
   861  			}
   862  			if !dryRun || createContext {
   863  				if contextName == "" {
   864  					contextName = "aws"
   865  				}
   866  				if err := contextCreate(contextName, namespace, serverCert); err != nil {
   867  					return err
   868  				}
   869  			}
   870  			return nil
   871  		}),
   872  	}
   873  	appendGlobalFlags(deployAmazon)
   874  	appendS3Flags(deployAmazon)
   875  	appendContextFlags(deployAmazon)
   876  	deployAmazon.Flags().StringVar(&cloudfrontDistribution, "cloudfront-distribution", "",
   877  		"Deploying on AWS with cloudfront is currently "+
   878  			"an alpha feature. No security restrictions have been"+
   879  			"applied to cloudfront, making all data public (obscured but not secured)")
   880  	deployAmazon.Flags().StringVar(&creds, "credentials", "", "Use the format \"<id>,<secret>[,<token>]\". You can get a token by running \"aws sts get-session-token\".")
   881  	deployAmazon.Flags().StringVar(&vault, "vault", "", "Use the format \"<address/hostport>,<role>,<token>\".")
   882  	deployAmazon.Flags().StringVar(&iamRole, "iam-role", "", fmt.Sprintf("Use the given IAM role for authorization, as opposed to using static credentials. The given role will be applied as the annotation %s, this used with a Kubernetes IAM role management system such as kube2iam allows you to give pachd credentials in a more secure way.", assets.IAMAnnotation))
   883  	commands = append(commands, cmdutil.CreateAlias(deployAmazon, "deploy amazon"))
   884  	commands = append(commands, cmdutil.CreateAlias(deployAmazon, "deploy aws"))
   885  
   886  	deployMicrosoft := &cobra.Command{
   887  		Use:   "{{alias}} <container> <account-name> <account-key> <disk-size>",
   888  		Short: "Deploy a Pachyderm cluster running on Microsoft Azure.",
   889  		Long: `Deploy a Pachyderm cluster running on Microsoft Azure.
   890    <container>: An Azure container where Pachyderm will store PFS data.
   891    <disk-size>: Size of persistent volumes, in GB (assumed to all be the same).`,
   892  		PreRun: deployPreRun,
   893  		Run: cmdutil.RunFixedArgs(4, func(args []string) (retErr error) {
   894  			start := time.Now()
   895  			startMetricsWait := _metrics.StartReportAndFlushUserAction("Deploy", start)
   896  			defer startMetricsWait()
   897  			defer func() {
   898  				finishMetricsWait := _metrics.FinishReportAndFlushUserAction("Deploy", retErr, start)
   899  				finishMetricsWait()
   900  			}()
   901  			if _, err := base64.StdEncoding.DecodeString(args[2]); err != nil {
   902  				return errors.Errorf("storage-account-key needs to be base64 encoded; instead got '%v'", args[2])
   903  			}
   904  			if opts.EtcdVolume != "" {
   905  				tempURI, err := url.ParseRequestURI(opts.EtcdVolume)
   906  				if err != nil {
   907  					return errors.Errorf("volume URI needs to be a well-formed URI; instead got '%v'", opts.EtcdVolume)
   908  				}
   909  				opts.EtcdVolume = tempURI.String()
   910  			}
   911  			volumeSize, err := strconv.Atoi(args[3])
   912  			if err != nil {
   913  				return errors.Errorf("volume size needs to be an integer; instead got %v", args[3])
   914  			}
   915  			var buf bytes.Buffer
   916  			container := strings.TrimPrefix(args[0], "wasb://")
   917  			accountName, accountKey := args[1], args[2]
   918  			if err = assets.WriteMicrosoftAssets(
   919  				encoder(outputFormat, &buf), opts, container, accountName, accountKey, volumeSize,
   920  			); err != nil {
   921  				return err
   922  			}
   923  			if err := kubectlCreate(dryRun, buf.Bytes(), opts); err != nil {
   924  				return err
   925  			}
   926  			if !dryRun || createContext {
   927  				if contextName == "" {
   928  					contextName = "azure"
   929  				}
   930  				if err := contextCreate(contextName, namespace, serverCert); err != nil {
   931  					return err
   932  				}
   933  			}
   934  			return nil
   935  		}),
   936  	}
   937  	appendGlobalFlags(deployMicrosoft)
   938  	appendContextFlags(deployMicrosoft)
   939  	commands = append(commands, cmdutil.CreateAlias(deployMicrosoft, "deploy microsoft"))
   940  	commands = append(commands, cmdutil.CreateAlias(deployMicrosoft, "deploy azure"))
   941  
   942  	deployStorageSecrets := func(data map[string][]byte) error {
   943  		cfg, err := config.Read(false, false)
   944  		if err != nil {
   945  			return err
   946  		}
   947  		_, activeContext, err := cfg.ActiveContext(true)
   948  		if err != nil {
   949  			return err
   950  		}
   951  
   952  		// clean up any empty, but non-nil strings in the data, since those will prevent those fields from getting merged when we do the patch
   953  		for k, v := range data {
   954  			if v != nil && len(v) == 0 {
   955  				delete(data, k)
   956  			}
   957  		}
   958  
   959  		var buf bytes.Buffer
   960  		if err = assets.WriteSecret(encoder(outputFormat, &buf), data, opts); err != nil {
   961  			return err
   962  		}
   963  		if dryRun {
   964  			_, err := os.Stdout.Write(buf.Bytes())
   965  			return err
   966  		}
   967  
   968  		s := buf.String()
   969  		return kubectl(&buf, activeContext, "patch", "secret", "pachyderm-storage-secret", "-p", s, "--namespace", opts.Namespace, "--type=merge")
   970  	}
   971  
   972  	deployStorageAmazon := &cobra.Command{
   973  		Use:    "{{alias}} <region> <access-key-id> <secret-access-key> [<session-token>]",
   974  		Short:  "Deploy credentials for the Amazon S3 storage provider.",
   975  		Long:   "Deploy credentials for the Amazon S3 storage provider, so that Pachyderm can ingress data from and egress data to it.",
   976  		PreRun: preRun,
   977  		Run: cmdutil.RunBoundedArgs(3, 4, func(args []string) error {
   978  			checkS3Flags()
   979  			var token string
   980  			if len(args) == 4 {
   981  				token = args[3]
   982  			}
   983  			// Setup advanced configuration.
   984  			advancedConfig := &obj.AmazonAdvancedConfiguration{
   985  				Retries:        retries,
   986  				Timeout:        timeout,
   987  				UploadACL:      uploadACL,
   988  				Reverse:        reverse,
   989  				PartSize:       partSize,
   990  				MaxUploadParts: maxUploadParts,
   991  				DisableSSL:     disableSSL,
   992  				NoVerifySSL:    noVerifySSL,
   993  				LogOptions:     logOptions,
   994  			}
   995  			return deployStorageSecrets(assets.AmazonSecret(args[0], "", args[1], args[2], token, "", "", advancedConfig))
   996  		}),
   997  	}
   998  	appendGlobalFlags(deployStorageAmazon)
   999  	appendS3Flags(deployStorageAmazon)
  1000  	commands = append(commands, cmdutil.CreateAlias(deployStorageAmazon, "deploy storage amazon"))
  1001  
  1002  	deployStorageGoogle := &cobra.Command{
  1003  		Use:    "{{alias}} <credentials-file>",
  1004  		Short:  "Deploy credentials for the Google Cloud storage provider.",
  1005  		Long:   "Deploy credentials for the Google Cloud storage provider, so that Pachyderm can ingress data from and egress data to it.",
  1006  		PreRun: preRun,
  1007  		Run: cmdutil.RunFixedArgs(1, func(args []string) error {
  1008  			credBytes, err := ioutil.ReadFile(args[0])
  1009  			if err != nil {
  1010  				return errors.Wrapf(err, "error reading credentials file %s", args[0])
  1011  			}
  1012  			return deployStorageSecrets(assets.GoogleSecret("", string(credBytes)))
  1013  		}),
  1014  	}
  1015  	appendGlobalFlags(deployStorageGoogle)
  1016  	commands = append(commands, cmdutil.CreateAlias(deployStorageGoogle, "deploy storage google"))
  1017  
  1018  	deployStorageAzure := &cobra.Command{
  1019  		Use:    "{{alias}} <account-name> <account-key>",
  1020  		Short:  "Deploy credentials for the Azure storage provider.",
  1021  		Long:   "Deploy credentials for the Azure storage provider, so that Pachyderm can ingress data from and egress data to it.",
  1022  		PreRun: preRun,
  1023  		Run: cmdutil.RunFixedArgs(2, func(args []string) error {
  1024  			return deployStorageSecrets(assets.MicrosoftSecret("", args[0], args[1]))
  1025  		}),
  1026  	}
  1027  	appendGlobalFlags(deployStorageAzure)
  1028  	commands = append(commands, cmdutil.CreateAlias(deployStorageAzure, "deploy storage microsoft"))
  1029  
  1030  	deployStorage := &cobra.Command{
  1031  		Short: "Deploy credentials for a particular storage provider.",
  1032  		Long:  "Deploy credentials for a particular storage provider, so that Pachyderm can ingress data from and egress data to it.",
  1033  	}
  1034  	commands = append(commands, cmdutil.CreateAlias(deployStorage, "deploy storage"))
  1035  
  1036  	listImages := &cobra.Command{
  1037  		Short:  "Output the list of images in a deployment.",
  1038  		Long:   "Output the list of images in a deployment.",
  1039  		PreRun: preRun,
  1040  		Run: cmdutil.RunFixedArgs(0, func(args []string) error {
  1041  			for _, image := range assets.Images(opts) {
  1042  				fmt.Println(image)
  1043  			}
  1044  			return nil
  1045  		}),
  1046  	}
  1047  	appendGlobalFlags(listImages)
  1048  	commands = append(commands, cmdutil.CreateAlias(listImages, "deploy list-images"))
  1049  
  1050  	exportImages := &cobra.Command{
  1051  		Use:    "{{alias}} <output-file>",
  1052  		Short:  "Export a tarball (to stdout) containing all of the images in a deployment.",
  1053  		Long:   "Export a tarball (to stdout) containing all of the images in a deployment.",
  1054  		PreRun: preRun,
  1055  		Run: cmdutil.RunFixedArgs(1, func(args []string) (retErr error) {
  1056  			file, err := os.Create(args[0])
  1057  			if err != nil {
  1058  				return err
  1059  			}
  1060  			defer func() {
  1061  				if err := file.Close(); err != nil && retErr == nil {
  1062  					retErr = err
  1063  				}
  1064  			}()
  1065  			return images.Export(opts, file)
  1066  		}),
  1067  	}
  1068  	appendGlobalFlags(exportImages)
  1069  	commands = append(commands, cmdutil.CreateAlias(exportImages, "deploy export-images"))
  1070  
  1071  	importImages := &cobra.Command{
  1072  		Use:    "{{alias}} <input-file>",
  1073  		Short:  "Import a tarball (from stdin) containing all of the images in a deployment and push them to a private registry.",
  1074  		Long:   "Import a tarball (from stdin) containing all of the images in a deployment and push them to a private registry.",
  1075  		PreRun: preRun,
  1076  		Run: cmdutil.RunFixedArgs(1, func(args []string) (retErr error) {
  1077  			file, err := os.Open(args[0])
  1078  			if err != nil {
  1079  				return err
  1080  			}
  1081  			defer func() {
  1082  				if err := file.Close(); err != nil && retErr == nil {
  1083  					retErr = err
  1084  				}
  1085  			}()
  1086  			return images.Import(opts, file)
  1087  		}),
  1088  	}
  1089  	appendGlobalFlags(importImages)
  1090  	commands = append(commands, cmdutil.CreateAlias(importImages, "deploy import-images"))
  1091  
  1092  	return commands
  1093  }
  1094  
  1095  // Cmds returns a list of cobra commands for deploying Pachyderm clusters.
  1096  func Cmds() []*cobra.Command {
  1097  	commands := standardDeployCmds()
  1098  
  1099  	var lbTLSHost string
  1100  	var lbTLSEmail string
  1101  	var dryRun bool
  1102  	var outputFormat string
  1103  	var jupyterhubChartVersion string
  1104  	var hubImage string
  1105  	var userImage string
  1106  	var namespace string
  1107  	deployIDE := &cobra.Command{
  1108  		Short: "Deploy the Pachyderm IDE.",
  1109  		Long:  "Deploy a JupyterHub-based IDE alongside the Pachyderm cluster.",
  1110  		Run: cmdutil.RunFixedArgs(0, func(args []string) (retErr error) {
  1111  			cfg, err := config.Read(false, false)
  1112  			if err != nil {
  1113  				return err
  1114  			}
  1115  			_, activeContext, err := cfg.ActiveContext(true)
  1116  			if err != nil {
  1117  				return err
  1118  			}
  1119  
  1120  			c, err := client.NewOnUserMachine("user")
  1121  			if err != nil {
  1122  				return errors.Wrapf(err, "error constructing pachyderm client")
  1123  			}
  1124  			defer c.Close()
  1125  
  1126  			enterpriseResp, err := c.Enterprise.GetState(c.Ctx(), &enterprise.GetStateRequest{})
  1127  			if err != nil {
  1128  				return errors.Wrapf(grpcutil.ScrubGRPC(err), "could not get Enterprise status")
  1129  			}
  1130  
  1131  			if enterpriseResp.State != enterprise.State_ACTIVE {
  1132  				return errors.New("Pachyderm Enterprise must be enabled to use this feature")
  1133  			}
  1134  
  1135  			authActive, err := c.IsAuthActive()
  1136  			if err != nil {
  1137  				return errors.Wrapf(grpcutil.ScrubGRPC(err), "could not check whether auth is active")
  1138  			}
  1139  			if !authActive {
  1140  				return errors.New("Pachyderm auth must be enabled to use this feature")
  1141  			}
  1142  
  1143  			whoamiResp, err := c.WhoAmI(c.Ctx(), &auth.WhoAmIRequest{})
  1144  			if err != nil {
  1145  				return errors.Wrapf(grpcutil.ScrubGRPC(err), "could not get the current logged in user")
  1146  			}
  1147  
  1148  			authTokenResp, err := c.GetAuthToken(c.Ctx(), &auth.GetAuthTokenRequest{
  1149  				Subject: whoamiResp.Username,
  1150  			})
  1151  			if err != nil {
  1152  				return errors.Wrapf(grpcutil.ScrubGRPC(err), "could not get an auth token")
  1153  			}
  1154  
  1155  			if jupyterhubChartVersion == "" {
  1156  				jupyterhubChartVersion = getCompatibleVersion("jupyterhub", "/jupyterhub", defaultIDEChartVersion)
  1157  			}
  1158  			if hubImage == "" || userImage == "" {
  1159  				ideVersion := getCompatibleVersion("ide", "/ide", defaultIDEVersion)
  1160  				if hubImage == "" {
  1161  					hubImage = fmt.Sprintf("%s:%s", defaultIDEHubImage, ideVersion)
  1162  				}
  1163  				if userImage == "" {
  1164  					userImage = fmt.Sprintf("%s:%s", defaultIDEUserImage, ideVersion)
  1165  				}
  1166  			}
  1167  
  1168  			hubImageName, hubImageTag := docker.ParseRepositoryTag(hubImage)
  1169  			userImageName, userImageTag := docker.ParseRepositoryTag(userImage)
  1170  
  1171  			values := map[string]interface{}{
  1172  				"hub": map[string]interface{}{
  1173  					"image": map[string]interface{}{
  1174  						"name": hubImageName,
  1175  						"tag":  hubImageTag,
  1176  					},
  1177  					"extraConfig": map[string]interface{}{
  1178  						"templates": "c.JupyterHub.template_paths = ['/app/templates']",
  1179  					},
  1180  				},
  1181  				"singleuser": map[string]interface{}{
  1182  					"image": map[string]interface{}{
  1183  						"name": userImageName,
  1184  						"tag":  userImageTag,
  1185  					},
  1186  					"defaultUrl": "/lab",
  1187  				},
  1188  				"auth": map[string]interface{}{
  1189  					"state": map[string]interface{}{
  1190  						"enabled":   true,
  1191  						"cryptoKey": generateSecureToken(16),
  1192  					},
  1193  					"type": "custom",
  1194  					"custom": map[string]interface{}{
  1195  						"className": "pachyderm_authenticator.PachydermAuthenticator",
  1196  						"config": map[string]interface{}{
  1197  							"pach_auth_token": authTokenResp.Token,
  1198  						},
  1199  					},
  1200  					"admin": map[string]interface{}{
  1201  						"users": []string{whoamiResp.Username},
  1202  					},
  1203  				},
  1204  				"proxy": map[string]interface{}{
  1205  					"secretToken": generateSecureToken(16),
  1206  				},
  1207  			}
  1208  
  1209  			if lbTLSHost != "" && lbTLSEmail != "" {
  1210  				values["https"] = map[string]interface{}{
  1211  					"hosts": []string{lbTLSHost},
  1212  					"letsencrypt": map[string]interface{}{
  1213  						"contactEmail": lbTLSEmail,
  1214  					},
  1215  				}
  1216  			}
  1217  
  1218  			if dryRun {
  1219  				var buf bytes.Buffer
  1220  				enc := encoder(outputFormat, &buf)
  1221  				if err = enc.Encode(values); err != nil {
  1222  					return err
  1223  				}
  1224  				_, err = os.Stdout.Write(buf.Bytes())
  1225  				return err
  1226  			}
  1227  
  1228  			// prefer explicit namespace
  1229  			if namespace != "" {
  1230  				activeContext.Namespace = namespace
  1231  			} else if activeContext.Namespace == "" {
  1232  				// check kubeconfig for a reasonable choice (or "default")
  1233  				activeContext.Namespace = getKubeNamespace()
  1234  			}
  1235  
  1236  			_, err = helm.Deploy(
  1237  				activeContext,
  1238  				"jupyterhub",
  1239  				"https://jupyterhub.github.io/helm-chart/",
  1240  				"pachyderm-ide",
  1241  				"jupyterhub/jupyterhub",
  1242  				jupyterhubChartVersion,
  1243  				values,
  1244  			)
  1245  			if err != nil {
  1246  				return errors.Wrapf(err, "failed to deploy Pachyderm IDE")
  1247  			}
  1248  
  1249  			fmt.Println(ideNotes)
  1250  			return nil
  1251  		}),
  1252  	}
  1253  	deployIDE.Flags().StringVar(&lbTLSHost, "lb-tls-host", "", "Hostname for minting a Let's Encrypt TLS cert on the load balancer")
  1254  	deployIDE.Flags().StringVar(&lbTLSEmail, "lb-tls-email", "", "Contact email for minting a Let's Encrypt TLS cert on the load balancer")
  1255  	deployIDE.Flags().BoolVar(&dryRun, "dry-run", false, "Don't actually deploy, instead just print the Helm config.")
  1256  	deployIDE.Flags().StringVarP(&outputFormat, "output", "o", "json", "Output format. One of: json|yaml")
  1257  	deployIDE.Flags().StringVar(&jupyterhubChartVersion, "jupyterhub-chart-version", "", "Version of the underlying Zero to JupyterHub with Kubernetes helm chart to use. By default this value is automatically derived.")
  1258  	deployIDE.Flags().StringVar(&hubImage, "hub-image", "", "Image for IDE hub. By default this value is automatically derived.")
  1259  	deployIDE.Flags().StringVar(&userImage, "user-image", "", "Image for IDE user environments. By default this value is automatically derived.")
  1260  	deployIDE.Flags().StringVar(&namespace, "namespace", "", "Kubernetes namespace to deploy IDE to.")
  1261  	commands = append(commands, cmdutil.CreateAlias(deployIDE, "deploy ide"))
  1262  
  1263  	deploy := &cobra.Command{
  1264  		Short: "Deploy a Pachyderm cluster.",
  1265  		Long:  "Deploy a Pachyderm cluster.",
  1266  	}
  1267  	commands = append(commands, cmdutil.CreateAlias(deploy, "deploy"))
  1268  
  1269  	var all bool
  1270  	var includingMetadata bool
  1271  	var includingIDE bool
  1272  	undeploy := &cobra.Command{
  1273  		Short: "Tear down a deployed Pachyderm cluster.",
  1274  		Long:  "Tear down a deployed Pachyderm cluster.",
  1275  		Run: cmdutil.RunFixedArgs(0, func(args []string) error {
  1276  			// TODO(ys): remove the `--namespace` flag here eventually
  1277  			if namespace != "" {
  1278  				fmt.Printf("WARNING: The `--namespace` flag is deprecated and will be removed in a future version. Please set the namespace in the pachyderm context instead: pachctl config update context `pachctl config get active-context` --namespace '%s'\n", namespace)
  1279  			}
  1280  			// TODO(ys): remove the `--all` flag here eventually
  1281  			if all {
  1282  				fmt.Printf("WARNING: The `--all` flag is deprecated and will be removed in a future version. Please use `--metadata` instead.\n")
  1283  				includingMetadata = true
  1284  			}
  1285  
  1286  			if includingMetadata {
  1287  				fmt.Fprintf(os.Stderr, `
  1288  You are going to delete persistent volumes where metadata is stored. If your
  1289  persistent volumes were dynamically provisioned (i.e. if you used the
  1290  "--dynamic-etcd-nodes" flag), the underlying volumes will be removed, making
  1291  metadata such as repos, commits, pipelines, and jobs unrecoverable. If your
  1292  persistent volume was manually provisioned (i.e. if you used the
  1293  "--static-etcd-volume" flag), the underlying volume will not be removed.
  1294  `)
  1295  			}
  1296  
  1297  			if ok, err := cmdutil.InteractiveConfirm(); err != nil {
  1298  				return err
  1299  			} else if !ok {
  1300  				return nil
  1301  			}
  1302  
  1303  			cfg, err := config.Read(false, false)
  1304  			if err != nil {
  1305  				return err
  1306  			}
  1307  			_, activeContext, err := cfg.ActiveContext(true)
  1308  			if err != nil {
  1309  				return err
  1310  			}
  1311  
  1312  			if namespace == "" {
  1313  				namespace = activeContext.Namespace
  1314  			}
  1315  
  1316  			assets := []string{
  1317  				"service",
  1318  				"replicationcontroller",
  1319  				"deployment",
  1320  				"serviceaccount",
  1321  				"secret",
  1322  				"statefulset",
  1323  				"clusterrole",
  1324  				"clusterrolebinding",
  1325  			}
  1326  			if includingMetadata {
  1327  				assets = append(assets, []string{
  1328  					"storageclass",
  1329  					"persistentvolumeclaim",
  1330  					"persistentvolume",
  1331  				}...)
  1332  			}
  1333  			if err := kubectl(nil, activeContext, "delete", strings.Join(assets, ","), "-l", "suite=pachyderm", "--namespace", namespace); err != nil {
  1334  				return err
  1335  			}
  1336  
  1337  			if includingIDE {
  1338  				// remove IDE
  1339  				if err = helm.Destroy(activeContext, "pachyderm-ide", namespace); err != nil {
  1340  					log.Errorf("failed to delete helm installation: %v", err)
  1341  				}
  1342  				ideAssets := []string{
  1343  					"replicaset",
  1344  					"deployment",
  1345  					"service",
  1346  					"pod",
  1347  				}
  1348  				if err = kubectl(nil, activeContext, "delete", strings.Join(ideAssets, ","), "-l", "app=jupyterhub", "--namespace", namespace); err != nil {
  1349  					return err
  1350  				}
  1351  			}
  1352  
  1353  			// remove the context from the config
  1354  			kubeConfig, err := config.RawKubeConfig()
  1355  			if err != nil {
  1356  				return err
  1357  			}
  1358  			kubeContext := kubeConfig.Contexts[kubeConfig.CurrentContext]
  1359  			if kubeContext != nil {
  1360  				cfg, err := config.Read(true, false)
  1361  				if err != nil {
  1362  					return err
  1363  				}
  1364  				ctx := &config.Context{
  1365  					ClusterName: kubeContext.Cluster,
  1366  					AuthInfo:    kubeContext.AuthInfo,
  1367  					Namespace:   namespace,
  1368  				}
  1369  
  1370  				// remove _all_ contexts associated with this
  1371  				// deployment
  1372  				configUpdated := false
  1373  				for {
  1374  					contextName, _ := findEquivalentContext(cfg, ctx)
  1375  					if contextName == "" {
  1376  						break
  1377  					}
  1378  					configUpdated = true
  1379  					delete(cfg.V2.Contexts, contextName)
  1380  					if contextName == cfg.V2.ActiveContext {
  1381  						cfg.V2.ActiveContext = ""
  1382  					}
  1383  				}
  1384  				if configUpdated {
  1385  					if err = cfg.Write(); err != nil {
  1386  						return err
  1387  					}
  1388  				}
  1389  			}
  1390  
  1391  			return nil
  1392  		}),
  1393  	}
  1394  	undeploy.Flags().BoolVarP(&all, "all", "a", false, "DEPRECATED: Use \"--metadata\" instead.")
  1395  	undeploy.Flags().BoolVarP(&includingMetadata, "metadata", "", false, `
  1396  Delete persistent volumes where metadata is stored. If your persistent volumes
  1397  were dynamically provisioned (i.e. if you used the "--dynamic-etcd-nodes"
  1398  flag), the underlying volumes will be removed, making metadata such as repos,
  1399  commits, pipelines, and jobs unrecoverable. If your persistent volume was
  1400  manually provisioned (i.e. if you used the "--static-etcd-volume" flag), the
  1401  underlying volume will not be removed.`)
  1402  	undeploy.Flags().BoolVarP(&includingIDE, "ide", "", false, "Delete the Pachyderm IDE deployment if it exists.")
  1403  	undeploy.Flags().StringVar(&namespace, "namespace", "", "Kubernetes namespace to undeploy Pachyderm from.")
  1404  	commands = append(commands, cmdutil.CreateAlias(undeploy, "undeploy"))
  1405  
  1406  	var updateDashDryRun bool
  1407  	var updateDashOutputFormat string
  1408  	updateDash := &cobra.Command{
  1409  		Short: "Update and redeploy the Pachyderm Dashboard at the latest compatible version.",
  1410  		Long:  "Update and redeploy the Pachyderm Dashboard at the latest compatible version.",
  1411  		Run: cmdutil.RunFixedArgs(0, func(args []string) error {
  1412  			cfg, err := config.Read(false, false)
  1413  			if err != nil {
  1414  				return err
  1415  			}
  1416  			_, activeContext, err := cfg.ActiveContext(false)
  1417  			if err != nil {
  1418  				return err
  1419  			}
  1420  
  1421  			// Undeploy the dash
  1422  			if !updateDashDryRun {
  1423  				if err := kubectl(nil, activeContext, "delete", "deploy", "-l", "suite=pachyderm,app=dash"); err != nil {
  1424  					return err
  1425  				}
  1426  				if err := kubectl(nil, activeContext, "delete", "svc", "-l", "suite=pachyderm,app=dash"); err != nil {
  1427  					return err
  1428  				}
  1429  			}
  1430  
  1431  			// Redeploy the dash
  1432  			var buf bytes.Buffer
  1433  			opts := &assets.AssetOpts{
  1434  				DashOnly:  true,
  1435  				DashImage: fmt.Sprintf("%s:%s", defaultDashImage, getCompatibleVersion("dash", "", defaultDashVersion)),
  1436  			}
  1437  			if err := assets.WriteDashboardAssets(
  1438  				encoder(updateDashOutputFormat, &buf), opts,
  1439  			); err != nil {
  1440  				return err
  1441  			}
  1442  			return kubectlCreate(updateDashDryRun, buf.Bytes(), opts)
  1443  		}),
  1444  	}
  1445  	updateDash.Flags().BoolVar(&updateDashDryRun, "dry-run", false, "Don't actually deploy Pachyderm Dash to Kubernetes, instead just print the manifest.")
  1446  	updateDash.Flags().StringVarP(&updateDashOutputFormat, "output", "o", "json", "Output format. One of: json|yaml")
  1447  	commands = append(commands, cmdutil.CreateAlias(updateDash, "update-dash"))
  1448  
  1449  	return commands
  1450  }
  1451  
  1452  // getCompatibleVersion gets the compatible version of another piece of
  1453  // software, or falls back to a default
  1454  func getCompatibleVersion(displayName, subpath, defaultValue string) string {
  1455  	var relVersion string
  1456  	// This is the branch where to look.
  1457  	// When a new version needs to be pushed we can just update the
  1458  	// compatibility file in pachyderm repo branch. A (re)deploy will pick it
  1459  	// up. To make this work we have to point the URL to the branch (not tag)
  1460  	// in the repo.
  1461  	branch := version.BranchFromVersion(version.Version)
  1462  	if version.IsCustomRelease(version.Version) {
  1463  		relVersion = version.PrettyPrintVersionNoAdditional(version.Version)
  1464  	} else {
  1465  		relVersion = version.PrettyPrintVersion(version.Version)
  1466  	}
  1467  
  1468  	url := fmt.Sprintf("https://raw.githubusercontent.com/pachyderm/pachyderm/compatibility%s/etc/%s/%s", branch, subpath, relVersion)
  1469  	resp, err := http.Get(url)
  1470  	if err != nil {
  1471  		log.Warningf("error looking up compatible version of %s, falling back to %s: %v", displayName, defaultValue, err)
  1472  		return defaultValue
  1473  	}
  1474  
  1475  	// Error on non-200; for the requests we're making, 200 is the only OK
  1476  	// state
  1477  	if resp.StatusCode != 200 {
  1478  		log.Warningf("error looking up compatible version of %s, falling back to %s: unexpected return code %d", displayName, defaultValue, resp.StatusCode)
  1479  		return defaultValue
  1480  	}
  1481  
  1482  	body, err := ioutil.ReadAll(resp.Body)
  1483  	if err != nil {
  1484  		log.Warningf("error looking up compatible version of %s, falling back to %s: %v", displayName, defaultValue, err)
  1485  		return defaultValue
  1486  	}
  1487  
  1488  	allVersions := strings.Split(strings.TrimSpace(string(body)), "\n")
  1489  	if len(allVersions) < 1 {
  1490  		log.Warningf("no compatible version of %s found, falling back to %s", displayName, defaultValue)
  1491  		return defaultValue
  1492  	}
  1493  	latestVersion := strings.TrimSpace(allVersions[len(allVersions)-1])
  1494  	return latestVersion
  1495  }