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(®istry, "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 }