github.com/pachyderm/pachyderm@v1.13.4/src/server/pkg/deploy/assets/assets.go (about) 1 package assets 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "path" 7 "path/filepath" 8 "strconv" 9 "strings" 10 11 "github.com/pachyderm/pachyderm/src/client" 12 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 13 "github.com/pachyderm/pachyderm/src/client/pkg/tls" 14 auth "github.com/pachyderm/pachyderm/src/server/auth/server" 15 pfs "github.com/pachyderm/pachyderm/src/server/pfs/server" 16 "github.com/pachyderm/pachyderm/src/server/pkg/obj" 17 "github.com/pachyderm/pachyderm/src/server/pkg/serde" 18 "github.com/pachyderm/pachyderm/src/server/pkg/uuid" 19 "github.com/pachyderm/pachyderm/src/server/pps/server/githook" 20 21 apps "k8s.io/api/apps/v1" 22 v1 "k8s.io/api/core/v1" 23 rbacv1 "k8s.io/api/rbac/v1" 24 "k8s.io/apimachinery/pkg/api/resource" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/util/intstr" 27 ) 28 29 const ( 30 // WorkerServiceAccountEnvVar is the name of the environment variable used to tell pachd 31 // what service account to assign to new worker RCs, for the purpose of 32 // creating S3 gateway services. 33 WorkerServiceAccountEnvVar = "WORKER_SERVICE_ACCOUNT" 34 // DefaultWorkerServiceAccountName is the default value to use if WorkerServiceAccountEnvVar is 35 // undefined (for compatibility purposes) 36 DefaultWorkerServiceAccountName = "pachyderm-worker" 37 workerRoleName = "pachyderm-worker" // Role given to worker Service Account 38 workerRoleBindingName = "pachyderm-worker" // Binding worker role to Service Account 39 ) 40 41 var ( 42 suite = "pachyderm" 43 44 pachdImage = "pachyderm/pachd" 45 // Using our own etcd image for now because there's a fix we need 46 // that hasn't been released, and which has been manually applied 47 // to the official v3.2.7 release. 48 etcdImage = "pachyderm/etcd:v3.3.5" 49 postgresImage = "postgres:13.0-alpine" 50 grpcProxyImage = "pachyderm/grpc-proxy:0.4.10" 51 dashName = "dash" 52 workerImage = "pachyderm/worker" 53 pauseImage = "gcr.io/google_containers/pause-amd64:3.0" 54 55 // ServiceAccountName is the name of Pachyderm's service account. 56 // It's public because it's needed by pps.APIServer to create the RCs for 57 // workers. 58 ServiceAccountName = "pachyderm" 59 etcdHeadlessServiceName = "etcd-headless" 60 etcdName = "etcd" 61 etcdVolumeName = "etcd-volume" 62 etcdVolumeClaimName = "etcd-storage" 63 // The storage class name to use when creating a new StorageClass for etcd. 64 defaultEtcdStorageClassName = "etcd-storage-class" 65 grpcProxyName = "grpc-proxy" 66 pachdName = "pachd" 67 // PrometheusPort hosts the prometheus stats for scraping 68 PrometheusPort = 656 69 70 postgresName = "postgres" 71 postgresVolumeName = "postgres-volume" 72 postgresVolumeClaimName = "postgres-storage" 73 defaultPostgresStorageClassName = "postgres-storage-class" 74 75 // Role & binding names, used for Roles or ClusterRoles and their associated 76 // bindings. 77 roleName = "pachyderm" 78 roleBindingName = "pachyderm" 79 // Policy rules to use for Pachyderm's Role or ClusterRole. 80 rolePolicyRules = []rbacv1.PolicyRule{{ 81 APIGroups: []string{""}, 82 Verbs: []string{"get", "list", "watch"}, 83 Resources: []string{"nodes", "pods", "pods/log", "endpoints"}, 84 }, { 85 APIGroups: []string{""}, 86 Verbs: []string{"get", "list", "watch", "create", "update", "delete"}, 87 Resources: []string{"replicationcontrollers", "services", "replicationcontrollers/scale"}, 88 }, { 89 APIGroups: []string{""}, 90 Verbs: []string{"get", "list", "watch", "create", "update", "delete", "deletecollection"}, 91 Resources: []string{"secrets"}, 92 }} 93 94 // The name of the local volume (mounted kubernetes secret) where pachd 95 // should read a TLS cert and private key for authenticating with clients 96 tlsVolumeName = "pachd-tls-cert" 97 // The name of the kubernetes secret mount in the TLS volume (see 98 // tlsVolumeName) 99 tlsSecretName = "pachd-tls-cert" 100 101 // Cmd used to launch etcd 102 etcdCmd = []string{ 103 "/usr/local/bin/etcd", 104 "--listen-client-urls=http://0.0.0.0:2379", 105 "--advertise-client-urls=http://0.0.0.0:2379", 106 "--data-dir=/var/data/etcd", 107 "--auto-compaction-retention=1", 108 "--max-txn-ops=10000", 109 "--max-request-bytes=52428800", //50mb 110 "--quota-backend-bytes=8589934592", //8gb 111 } 112 113 // IAMAnnotation is the annotation used for the IAM role, this can work 114 // with something like kube2iam as an alternative way to provide 115 // credentials. 116 IAMAnnotation = "iam.amazonaws.com/role" 117 ) 118 119 // Backend is the type used to enumerate what system provides object storage or 120 // persistent disks for the cluster (each can be configured separately). 121 type Backend int 122 123 const ( 124 // LocalBackend is used in development (e.g. minikube) which provides a volume on the host machine 125 LocalBackend Backend = iota 126 127 // AmazonBackend uses S3 for object storage 128 AmazonBackend 129 130 // GoogleBackend uses GCS for object storage 131 GoogleBackend 132 133 // MicrosoftBackend uses Azure blobs for object storage 134 MicrosoftBackend 135 136 // MinioBackend uses the Minio client for object storage, but it can point to any S3-compatible API 137 MinioBackend 138 139 // S3CustomArgs uses the S3 or Minio clients for object storage with custom endpoint configuration 140 S3CustomArgs = 6 141 ) 142 143 // TLSOpts indicates the cert and key file that Pachd should use to 144 // authenticate with clients 145 type TLSOpts struct { 146 ServerCert string 147 ServerKey string 148 } 149 150 // FeatureFlags are flags for experimental features. 151 type FeatureFlags struct { 152 // StorageV2, if true, will make Pachyderm use the new storage layer. 153 StorageV2 bool 154 } 155 156 const ( 157 // UploadConcurrencyLimitEnvVar is the environment variable for the upload concurrency limit. 158 UploadConcurrencyLimitEnvVar = "STORAGE_UPLOAD_CONCURRENCY_LIMIT" 159 160 // PutFileConcurrencyLimitEnvVar is the environment variable for the PutFile concurrency limit. 161 PutFileConcurrencyLimitEnvVar = "STORAGE_PUT_FILE_CONCURRENCY_LIMIT" 162 163 // StorageV2EnvVar is the environment variable for enabling V2 storage. 164 StorageV2EnvVar = "STORAGE_V2" 165 ) 166 167 const ( 168 // DefaultUploadConcurrencyLimit is the default maximum number of concurrent object storage uploads. 169 // (bryce) this default is set here and in the service env config, need to figure out how to refactor 170 // this to be in one place. 171 DefaultUploadConcurrencyLimit = 100 172 173 // DefaultPutFileConcurrencyLimit is the default maximum number of concurrent files that can be uploaded over GRPC or downloaded from external sources (ex. HTTP or blob storage). 174 DefaultPutFileConcurrencyLimit = 100 175 ) 176 177 // StorageOpts are options that are applicable to the storage layer. 178 type StorageOpts struct { 179 UploadConcurrencyLimit int 180 PutFileConcurrencyLimit int 181 } 182 183 const ( 184 // RequireCriticalServersOnlyEnvVar is the environment variable for requiring critical servers only. 185 RequireCriticalServersOnlyEnvVar = "REQUIRE_CRITICAL_SERVERS_ONLY" 186 ) 187 188 const ( 189 // DefaultRequireCriticalServersOnly is the default for requiring critical servers only. 190 // (bryce) this default is set here and in the service env config, need to figure out how to refactor 191 // this to be in one place. 192 DefaultRequireCriticalServersOnly = false 193 ) 194 195 // AssetOpts are options that are applicable to all the asset types. 196 type AssetOpts struct { 197 FeatureFlags 198 StorageOpts 199 PachdShards uint64 200 Version string 201 LogLevel string 202 Metrics bool 203 Dynamic bool 204 EtcdNodes int 205 EtcdVolume string 206 PostgresNodes int 207 PostgresVolume string 208 DashOnly bool 209 NoDash bool 210 DashImage string 211 Registry string 212 EtcdPrefix string 213 PachdPort int32 214 TracePort int32 215 HTTPPort int32 216 PeerPort int32 217 218 // NoGuaranteed will not generate assets that have both resource limits and 219 // resource requests set which causes kubernetes to give the pods 220 // guaranteed QoS. Guaranteed QoS generally leads to more stable clusters 221 // but on smaller test clusters such as those run on minikube it doesn't 222 // help much and may cause more instability than it prevents. 223 NoGuaranteed bool 224 225 // DisableAuthentication stops Pachyderm's authentication service 226 // from talking to GitHub, for testing. Instead users can authenticate 227 // simply by providing a username. 228 DisableAuthentication bool 229 230 // BlockCacheSize is the amount of memory each PachD node allocates towards 231 // its cache of PFS blocks. If empty, assets.go will choose a default size. 232 BlockCacheSize string 233 234 // PachdCPURequest is the amount of CPU we request for each pachd node. If 235 // empty, assets.go will choose a default size. 236 PachdCPURequest string 237 238 // PachdNonCacheMemRequest is the amount of memory we request for each 239 // pachd node in addition to BlockCacheSize. If empty, assets.go will choose 240 // a default size. 241 PachdNonCacheMemRequest string 242 243 // EtcdCPURequest is the amount of CPU (in cores) we request for each etcd 244 // node. If empty, assets.go will choose a default size. 245 EtcdCPURequest string 246 247 // EtcdMemRequest is the amount of memory we request for each etcd node. If 248 // empty, assets.go will choose a default size. 249 EtcdMemRequest string 250 251 // EtcdStorageClassName is the name of an existing StorageClass to use when 252 // creating a StatefulSet for dynamic etcd storage. If unset, a new 253 // StorageClass will be created for the StatefulSet. 254 EtcdStorageClassName string 255 256 // PostgresCPURequest is the amount of CPU (in cores) we request for each 257 // postgres node. If empty, assets.go will choose a default size. 258 PostgresCPURequest string 259 260 // PostgresMemRequest is the amount of memory we request for each postgres 261 // node. If empty, assets.go will choose a default size. 262 PostgresMemRequest string 263 264 // PostgresStorageClassName is the name of an existing StorageClass to use when 265 // creating a StatefulSet for dynamic postgres storage. If unset, a new 266 // StorageClass will be created for the StatefulSet. 267 PostgresStorageClassName string 268 269 // IAM role that the Pachyderm deployment should assume when talking to AWS 270 // services (if using kube2iam + metadata service + IAM role to delegate 271 // permissions to pachd via its instance). 272 // This is in AssetOpts rather than AmazonCreds because it must be passed 273 // as an annotation on the pachd pod rather than as a k8s secret 274 IAMRole string 275 276 // ImagePullSecret specifies an image pull secret that gets attached to the 277 // various deployments so that their images can be pulled from a private 278 // registry. 279 ImagePullSecret string 280 281 // NoRBAC, if true, will disable creation of RBAC assets. 282 NoRBAC bool 283 284 // LocalRoles, if true, uses Role and RoleBinding instead of ClusterRole and 285 // ClusterRoleBinding. 286 LocalRoles bool 287 288 // Namespace is the kubernetes namespace to deploy to. 289 Namespace string 290 291 // NoExposeDockerSocket if true prevents pipelines from accessing the docker socket. 292 NoExposeDockerSocket bool 293 294 // ExposeObjectAPI, if set, causes pachd to serve Object/Block API requests on 295 // its public port. This should generally be false in production (it breaks 296 // auth) but is needed by tests 297 ExposeObjectAPI bool 298 299 // If set, the files indictated by 'TLS.ServerCert' and 'TLS.ServerKey' are 300 // placed into a Kubernetes secret and used by pachd nodes to authenticate 301 // during TLS 302 TLS *TLSOpts 303 304 // Sets the cluster deployment ID. If unset, this will be a randomly 305 // generated UUID without dashes. 306 ClusterDeploymentID string 307 308 // RequireCriticalServersOnly is true when only the critical Pachd servers 309 // are required to startup and run without error. 310 RequireCriticalServersOnly bool 311 312 // WorkerServiceAccountName is the name of the service account that will be 313 // used in the worker pods for creating S3 gateways. 314 WorkerServiceAccountName string 315 } 316 317 // replicas lets us create a pointer to a non-zero int32 in-line. This is 318 // helpful because the Replicas field of many specs expectes an *int32 319 func replicas(r int32) *int32 { 320 return &r 321 } 322 323 // fillDefaultResourceRequests sets any of: 324 // opts.BlockCacheSize 325 // opts.PachdNonCacheMemRequest 326 // opts.PachdCPURequest 327 // opts.EtcdCPURequest 328 // opts.EtcdMemRequest 329 // that are unset in 'opts' to the appropriate default ('persistentDiskBackend' 330 // just used to determine if this is a local deployment, and if so, make the 331 // resource requests smaller) 332 func fillDefaultResourceRequests(opts *AssetOpts, persistentDiskBackend Backend) { 333 if persistentDiskBackend == LocalBackend { 334 // For local deployments, we set the resource requirements and cache sizes 335 // low so that pachyderm clusters will fit inside e.g. minikube or travis 336 if opts.BlockCacheSize == "" { 337 opts.BlockCacheSize = "256M" 338 } 339 if opts.PachdNonCacheMemRequest == "" { 340 opts.PachdNonCacheMemRequest = "256M" 341 } 342 if opts.PachdCPURequest == "" { 343 opts.PachdCPURequest = "0.25" 344 } 345 346 if opts.EtcdMemRequest == "" { 347 opts.EtcdMemRequest = "512M" 348 } 349 if opts.EtcdCPURequest == "" { 350 opts.EtcdCPURequest = "0.25" 351 } 352 353 if opts.PostgresMemRequest == "" { 354 opts.PostgresMemRequest = "256M" 355 } 356 if opts.PostgresCPURequest == "" { 357 opts.PostgresCPURequest = "0.25" 358 } 359 } else { 360 // For non-local deployments, we set the resource requirements and cache 361 // sizes higher, so that the cluster is stable and performant 362 if opts.BlockCacheSize == "" { 363 opts.BlockCacheSize = "1G" 364 } 365 if opts.PachdNonCacheMemRequest == "" { 366 opts.PachdNonCacheMemRequest = "2G" 367 } 368 if opts.PachdCPURequest == "" { 369 opts.PachdCPURequest = "1" 370 } 371 372 if opts.EtcdMemRequest == "" { 373 opts.EtcdMemRequest = "2G" 374 } 375 if opts.EtcdCPURequest == "" { 376 opts.EtcdCPURequest = "1" 377 } 378 379 if opts.PostgresMemRequest == "" { 380 opts.PostgresMemRequest = "2G" 381 } 382 if opts.PostgresCPURequest == "" { 383 opts.PostgresCPURequest = "1" 384 } 385 } 386 } 387 388 // ServiceAccounts returns a kubernetes service account for use with Pachyderm. 389 func ServiceAccounts(opts *AssetOpts) []*v1.ServiceAccount { 390 return []*v1.ServiceAccount{ 391 // Pachd service account 392 { 393 TypeMeta: metav1.TypeMeta{ 394 Kind: "ServiceAccount", 395 APIVersion: "v1", 396 }, 397 ObjectMeta: objectMeta(ServiceAccountName, labels(""), nil, opts.Namespace), 398 }, 399 400 // worker service account 401 { 402 TypeMeta: metav1.TypeMeta{ 403 Kind: "ServiceAccount", 404 APIVersion: "v1", 405 }, 406 ObjectMeta: objectMeta(opts.WorkerServiceAccountName, labels(""), nil, opts.Namespace), 407 }, 408 } 409 } 410 411 // ClusterRole returns a ClusterRole that should be bound to the Pachyderm service account. 412 func ClusterRole(opts *AssetOpts) *rbacv1.ClusterRole { 413 return &rbacv1.ClusterRole{ 414 TypeMeta: metav1.TypeMeta{ 415 Kind: "ClusterRole", 416 APIVersion: "rbac.authorization.k8s.io/v1", 417 }, 418 ObjectMeta: objectMeta(roleName, labels(""), nil, opts.Namespace), 419 Rules: rolePolicyRules, 420 } 421 } 422 423 // ClusterRoleBinding returns a ClusterRoleBinding that binds Pachyderm's 424 // ClusterRole to its ServiceAccount. 425 func ClusterRoleBinding(opts *AssetOpts) *rbacv1.ClusterRoleBinding { 426 // cluster role bindings are global to the cluster and not associated with a namespace, 427 // so if pachyderm is deployed multiple times in different namespaces, each will try to 428 // create its own cluster role binding. To avoid interference, include namespace in the name 429 scopedName := fmt.Sprintf("%s-%s", roleBindingName, opts.Namespace) 430 return &rbacv1.ClusterRoleBinding{ 431 TypeMeta: metav1.TypeMeta{ 432 Kind: "ClusterRoleBinding", 433 APIVersion: "rbac.authorization.k8s.io/v1", 434 }, 435 ObjectMeta: objectMeta(scopedName, labels(""), nil, opts.Namespace), 436 Subjects: []rbacv1.Subject{{ 437 Kind: "ServiceAccount", 438 Name: ServiceAccountName, 439 Namespace: opts.Namespace, 440 }}, 441 RoleRef: rbacv1.RoleRef{ 442 Kind: "ClusterRole", 443 Name: roleName, 444 }, 445 } 446 } 447 448 // Role returns a Role that should be bound to the Pachyderm service account. 449 func Role(opts *AssetOpts) *rbacv1.Role { 450 return &rbacv1.Role{ 451 TypeMeta: metav1.TypeMeta{ 452 Kind: "Role", 453 APIVersion: "rbac.authorization.k8s.io/v1", 454 }, 455 ObjectMeta: objectMeta(roleName, labels(""), nil, opts.Namespace), 456 Rules: rolePolicyRules, 457 } 458 } 459 460 // workerRole returns a Role bound to the Pachyderm worker service account 461 // (used by workers to create an s3 gateway k8s service for each job) 462 func workerRole(opts *AssetOpts) *rbacv1.Role { 463 return &rbacv1.Role{ 464 TypeMeta: metav1.TypeMeta{ 465 Kind: "Role", 466 APIVersion: "rbac.authorization.k8s.io/v1", 467 }, 468 ObjectMeta: objectMeta(workerRoleName, labels(""), nil, opts.Namespace), 469 Rules: []rbacv1.PolicyRule{{ 470 APIGroups: []string{""}, 471 Verbs: []string{"get", "list", "update", "create", "delete"}, 472 Resources: []string{"services"}, 473 }}, 474 } 475 } 476 477 // RoleBinding returns a RoleBinding that binds Pachyderm's Role to its 478 // ServiceAccount. 479 func RoleBinding(opts *AssetOpts) *rbacv1.RoleBinding { 480 return &rbacv1.RoleBinding{ 481 TypeMeta: metav1.TypeMeta{ 482 Kind: "RoleBinding", 483 APIVersion: "rbac.authorization.k8s.io/v1", 484 }, 485 ObjectMeta: objectMeta(roleBindingName, labels(""), nil, opts.Namespace), 486 Subjects: []rbacv1.Subject{{ 487 Kind: "ServiceAccount", 488 Name: ServiceAccountName, 489 Namespace: opts.Namespace, 490 }}, 491 RoleRef: rbacv1.RoleRef{ 492 Kind: "Role", 493 Name: roleName, 494 }, 495 } 496 } 497 498 // RoleBinding returns a RoleBinding that binds Pachyderm's workerRole to its 499 // worker service account (assets.WorkerServiceAccountName) 500 func workerRoleBinding(opts *AssetOpts) *rbacv1.RoleBinding { 501 return &rbacv1.RoleBinding{ 502 TypeMeta: metav1.TypeMeta{ 503 Kind: "RoleBinding", 504 APIVersion: "rbac.authorization.k8s.io/v1", 505 }, 506 ObjectMeta: objectMeta(workerRoleBindingName, labels(""), nil, opts.Namespace), 507 Subjects: []rbacv1.Subject{{ 508 Kind: "ServiceAccount", 509 Name: opts.WorkerServiceAccountName, 510 Namespace: opts.Namespace, 511 }}, 512 RoleRef: rbacv1.RoleRef{ 513 Kind: "Role", 514 Name: workerRoleName, 515 }, 516 } 517 } 518 519 // GetBackendSecretVolumeAndMount returns a properly configured Volume and 520 // VolumeMount object given a backend. The backend needs to be one of the 521 // constants defined in pfs/server. 522 func GetBackendSecretVolumeAndMount(backend string) (v1.Volume, v1.VolumeMount) { 523 return v1.Volume{ 524 Name: client.StorageSecretName, 525 VolumeSource: v1.VolumeSource{ 526 Secret: &v1.SecretVolumeSource{ 527 SecretName: client.StorageSecretName, 528 }, 529 }, 530 }, v1.VolumeMount{ 531 Name: client.StorageSecretName, 532 MountPath: "/" + client.StorageSecretName, 533 } 534 } 535 536 // GetSecretEnvVars returns the environment variable specs for the storage secret. 537 func GetSecretEnvVars(storageBackend string) []v1.EnvVar { 538 var envVars []v1.EnvVar 539 if storageBackend != "" { 540 envVars = append(envVars, v1.EnvVar{ 541 Name: obj.StorageBackendEnvVar, 542 Value: storageBackend, 543 }) 544 } 545 trueVal := true 546 for _, e := range obj.EnvVarToSecretKey { 547 envVars = append(envVars, v1.EnvVar{ 548 Name: e.Key, 549 ValueFrom: &v1.EnvVarSource{ 550 SecretKeyRef: &v1.SecretKeySelector{ 551 LocalObjectReference: v1.LocalObjectReference{ 552 Name: client.StorageSecretName, 553 }, 554 Key: e.Value, 555 Optional: &trueVal, 556 }, 557 }, 558 }) 559 } 560 return envVars 561 } 562 563 func getStorageEnvVars(opts *AssetOpts) []v1.EnvVar { 564 return []v1.EnvVar{ 565 {Name: UploadConcurrencyLimitEnvVar, Value: strconv.Itoa(opts.StorageOpts.UploadConcurrencyLimit)}, 566 {Name: PutFileConcurrencyLimitEnvVar, Value: strconv.Itoa(opts.StorageOpts.PutFileConcurrencyLimit)}, 567 {Name: StorageV2EnvVar, Value: strconv.FormatBool(opts.FeatureFlags.StorageV2)}, 568 } 569 } 570 571 func versionedPachdImage(opts *AssetOpts) string { 572 if opts.Version != "" { 573 return fmt.Sprintf("%s:%s", pachdImage, opts.Version) 574 } 575 return pachdImage 576 } 577 578 func versionedWorkerImage(opts *AssetOpts) string { 579 if opts.Version != "" { 580 return fmt.Sprintf("%s:%s", workerImage, opts.Version) 581 } 582 return workerImage 583 } 584 585 func imagePullSecrets(opts *AssetOpts) []v1.LocalObjectReference { 586 var result []v1.LocalObjectReference 587 if opts.ImagePullSecret != "" { 588 result = append(result, v1.LocalObjectReference{Name: opts.ImagePullSecret}) 589 } 590 return result 591 } 592 593 // PachdDeployment returns a pachd k8s Deployment. 594 func PachdDeployment(opts *AssetOpts, objectStoreBackend Backend, hostPath string) *apps.Deployment { 595 // set port defaults 596 if opts.PachdPort == 0 { 597 opts.PachdPort = 650 598 } 599 if opts.TracePort == 0 { 600 opts.TracePort = 651 601 } 602 if opts.HTTPPort == 0 { 603 opts.HTTPPort = 652 604 } 605 if opts.PeerPort == 0 { 606 opts.PeerPort = 653 607 } 608 mem := resource.MustParse(opts.BlockCacheSize) 609 mem.Add(resource.MustParse(opts.PachdNonCacheMemRequest)) 610 cpu := resource.MustParse(opts.PachdCPURequest) 611 image := AddRegistry(opts.Registry, versionedPachdImage(opts)) 612 volumes := []v1.Volume{ 613 { 614 Name: "pach-disk", 615 }, 616 } 617 volumeMounts := []v1.VolumeMount{ 618 { 619 Name: "pach-disk", 620 MountPath: "/pach", 621 }, 622 } 623 624 // Set up storage options 625 var backendEnvVar string 626 var storageHostPath string 627 switch objectStoreBackend { 628 case LocalBackend: 629 storageHostPath = path.Join(hostPath, "pachd") 630 pathType := v1.HostPathDirectoryOrCreate 631 volumes[0].HostPath = &v1.HostPathVolumeSource{ 632 Path: storageHostPath, 633 Type: &pathType, 634 } 635 backendEnvVar = pfs.LocalBackendEnvVar 636 case MinioBackend: 637 backendEnvVar = pfs.MinioBackendEnvVar 638 case AmazonBackend: 639 backendEnvVar = pfs.AmazonBackendEnvVar 640 case GoogleBackend: 641 backendEnvVar = pfs.GoogleBackendEnvVar 642 case MicrosoftBackend: 643 backendEnvVar = pfs.MicrosoftBackendEnvVar 644 } 645 volume, mount := GetBackendSecretVolumeAndMount(backendEnvVar) 646 volumes = append(volumes, volume) 647 volumeMounts = append(volumeMounts, mount) 648 if opts.TLS != nil { 649 volumes = append(volumes, v1.Volume{ 650 Name: tlsVolumeName, 651 VolumeSource: v1.VolumeSource{ 652 Secret: &v1.SecretVolumeSource{ 653 SecretName: tlsSecretName, 654 }, 655 }, 656 }) 657 volumeMounts = append(volumeMounts, v1.VolumeMount{ 658 Name: tlsVolumeName, 659 MountPath: tls.VolumePath, 660 }) 661 } 662 resourceRequirements := v1.ResourceRequirements{ 663 Requests: v1.ResourceList{ 664 v1.ResourceCPU: cpu, 665 v1.ResourceMemory: mem, 666 }, 667 } 668 if !opts.NoGuaranteed { 669 resourceRequirements.Limits = v1.ResourceList{ 670 v1.ResourceCPU: cpu, 671 v1.ResourceMemory: mem, 672 } 673 } 674 if opts.ClusterDeploymentID == "" { 675 opts.ClusterDeploymentID = uuid.NewWithoutDashes() 676 } 677 envVars := []v1.EnvVar{ 678 {Name: "PACH_ROOT", Value: "/pach"}, 679 {Name: "ETCD_PREFIX", Value: opts.EtcdPrefix}, 680 {Name: "NUM_SHARDS", Value: fmt.Sprintf("%d", opts.PachdShards)}, 681 {Name: "STORAGE_BACKEND", Value: backendEnvVar}, 682 {Name: "STORAGE_HOST_PATH", Value: storageHostPath}, 683 {Name: "WORKER_IMAGE", Value: AddRegistry(opts.Registry, versionedWorkerImage(opts))}, 684 {Name: "IMAGE_PULL_SECRET", Value: opts.ImagePullSecret}, 685 {Name: "WORKER_SIDECAR_IMAGE", Value: image}, 686 {Name: "WORKER_IMAGE_PULL_POLICY", Value: "IfNotPresent"}, 687 {Name: WorkerServiceAccountEnvVar, Value: opts.WorkerServiceAccountName}, 688 {Name: "PACHD_VERSION", Value: opts.Version}, 689 {Name: "METRICS", Value: strconv.FormatBool(opts.Metrics)}, 690 {Name: "LOG_LEVEL", Value: opts.LogLevel}, 691 {Name: "BLOCK_CACHE_BYTES", Value: opts.BlockCacheSize}, 692 {Name: "IAM_ROLE", Value: opts.IAMRole}, 693 {Name: "NO_EXPOSE_DOCKER_SOCKET", Value: strconv.FormatBool(opts.NoExposeDockerSocket)}, 694 {Name: auth.DisableAuthenticationEnvVar, Value: strconv.FormatBool(opts.DisableAuthentication)}, 695 { 696 Name: "PACH_NAMESPACE", 697 ValueFrom: &v1.EnvVarSource{ 698 FieldRef: &v1.ObjectFieldSelector{ 699 APIVersion: "v1", 700 FieldPath: "metadata.namespace", 701 }, 702 }, 703 }, 704 { 705 Name: "PACHD_MEMORY_REQUEST", 706 ValueFrom: &v1.EnvVarSource{ 707 ResourceFieldRef: &v1.ResourceFieldSelector{ 708 ContainerName: "pachd", 709 Resource: "requests.memory", 710 }, 711 }, 712 }, 713 {Name: "EXPOSE_OBJECT_API", Value: strconv.FormatBool(opts.ExposeObjectAPI)}, 714 {Name: "CLUSTER_DEPLOYMENT_ID", Value: opts.ClusterDeploymentID}, 715 {Name: RequireCriticalServersOnlyEnvVar, Value: strconv.FormatBool(opts.RequireCriticalServersOnly)}, 716 { 717 Name: "PACHD_POD_NAME", 718 ValueFrom: &v1.EnvVarSource{ 719 FieldRef: &v1.ObjectFieldSelector{ 720 APIVersion: "v1", 721 FieldPath: "metadata.name", 722 }, 723 }, 724 }, 725 // TODO: Setting the default explicitly to ensure the environment variable is set since we pull directly 726 // from it for setting up the worker client. Probably should not be pulling directly from environment variables. 727 { 728 Name: client.PPSWorkerPortEnv, 729 Value: "80", 730 }, 731 } 732 envVars = append(envVars, GetSecretEnvVars("")...) 733 envVars = append(envVars, getStorageEnvVars(opts)...) 734 return &apps.Deployment{ 735 TypeMeta: metav1.TypeMeta{ 736 Kind: "Deployment", 737 APIVersion: "apps/v1", 738 }, 739 ObjectMeta: objectMeta(pachdName, labels(pachdName), nil, opts.Namespace), 740 Spec: apps.DeploymentSpec{ 741 Replicas: replicas(1), 742 Selector: &metav1.LabelSelector{ 743 MatchLabels: labels(pachdName), 744 }, 745 Template: v1.PodTemplateSpec{ 746 ObjectMeta: objectMeta(pachdName, labels(pachdName), 747 map[string]string{IAMAnnotation: opts.IAMRole}, opts.Namespace), 748 Spec: v1.PodSpec{ 749 Containers: []v1.Container{ 750 { 751 Name: pachdName, 752 Image: image, 753 Command: []string{"/pachd"}, 754 Env: envVars, 755 Ports: []v1.ContainerPort{ 756 { 757 ContainerPort: opts.PachdPort, // also set in cmd/pachd/main.go 758 Protocol: "TCP", 759 Name: "api-grpc-port", 760 }, 761 { 762 ContainerPort: opts.TracePort, // also set in cmd/pachd/main.go 763 Name: "trace-port", 764 }, 765 { 766 ContainerPort: opts.HTTPPort, // also set in cmd/pachd/main.go 767 Protocol: "TCP", 768 Name: "api-http-port", 769 }, 770 { 771 ContainerPort: opts.PeerPort, // also set in cmd/pachd/main.go 772 Protocol: "TCP", 773 Name: "peer-port", 774 }, 775 { 776 ContainerPort: githook.GitHookPort, 777 Protocol: "TCP", 778 Name: "api-git-port", 779 }, 780 { 781 ContainerPort: auth.SamlPort, 782 Protocol: "TCP", 783 Name: "saml-port", 784 }, 785 { 786 ContainerPort: auth.OidcPort, 787 Protocol: "TCP", 788 Name: "oidc-port", 789 }, 790 { 791 ContainerPort: int32(PrometheusPort), 792 Protocol: "TCP", 793 Name: "prom-metrics", 794 }, 795 }, 796 VolumeMounts: volumeMounts, 797 ImagePullPolicy: "IfNotPresent", 798 Resources: resourceRequirements, 799 ReadinessProbe: &v1.Probe{ 800 Handler: v1.Handler{ 801 Exec: &v1.ExecAction{ 802 Command: []string{"/pachd", "--readiness"}, 803 }, 804 }, 805 }, 806 }, 807 }, 808 ServiceAccountName: ServiceAccountName, 809 Volumes: volumes, 810 ImagePullSecrets: imagePullSecrets(opts), 811 }, 812 }, 813 }, 814 } 815 } 816 817 // PachdService returns a pachd service. 818 func PachdService(opts *AssetOpts) *v1.Service { 819 prometheusAnnotations := map[string]string{ 820 "prometheus.io/scrape": "true", 821 "prometheus.io/port": strconv.Itoa(PrometheusPort), 822 } 823 return &v1.Service{ 824 TypeMeta: metav1.TypeMeta{ 825 Kind: "Service", 826 APIVersion: "v1", 827 }, 828 ObjectMeta: objectMeta(pachdName, labels(pachdName), prometheusAnnotations, opts.Namespace), 829 Spec: v1.ServiceSpec{ 830 Type: v1.ServiceTypeNodePort, 831 Selector: map[string]string{ 832 "app": pachdName, 833 }, 834 Ports: []v1.ServicePort{ 835 // NOTE: do not put any new ports before `api-grpc-port`, as 836 // it'll change k8s SERVICE_PORT env var values 837 { 838 Port: 650, // also set in cmd/pachd/main.go 839 Name: "api-grpc-port", 840 NodePort: 30650, 841 }, 842 { 843 Port: 651, // also set in cmd/pachd/main.go 844 Name: "trace-port", 845 NodePort: 30651, 846 }, 847 { 848 Port: 652, // also set in cmd/pachd/main.go 849 Name: "api-http-port", 850 NodePort: 30652, 851 }, 852 { 853 Port: auth.SamlPort, 854 Name: "saml-port", 855 NodePort: 30000 + auth.SamlPort, 856 }, 857 { 858 Port: auth.OidcPort, 859 Name: "oidc-port", 860 NodePort: 30000 + auth.OidcPort, 861 }, 862 { 863 Port: githook.GitHookPort, 864 Name: "api-git-port", 865 NodePort: githook.NodePort(), 866 }, 867 { 868 Port: 600, // also set in cmd/pachd/main.go 869 Name: "s3gateway-port", 870 NodePort: 30600, 871 }, 872 { 873 Port: 656, 874 Name: "prom-metrics", 875 NodePort: 30656, 876 Protocol: v1.ProtocolTCP, 877 TargetPort: intstr.FromInt(656), 878 }, 879 }, 880 }, 881 } 882 } 883 884 // PachdPeerService returns an internal pachd service. This service will 885 // reference the PeerPorr, which does not employ TLS even if cluster TLS is 886 // enabled. Because of this, the service is a `ClusterIP` type (i.e. not 887 // exposed outside of the cluster.) 888 func PachdPeerService(opts *AssetOpts) *v1.Service { 889 return &v1.Service{ 890 TypeMeta: metav1.TypeMeta{ 891 Kind: "Service", 892 APIVersion: "v1", 893 }, 894 ObjectMeta: objectMeta(fmt.Sprintf("%s-peer", pachdName), labels(pachdName), map[string]string{}, opts.Namespace), 895 Spec: v1.ServiceSpec{ 896 Type: v1.ServiceTypeClusterIP, 897 Selector: map[string]string{ 898 "app": pachdName, 899 }, 900 Ports: []v1.ServicePort{ 901 { 902 Port: 30653, 903 Name: "api-grpc-peer-port", 904 TargetPort: intstr.FromInt(653), // also set in cmd/pachd/main.go 905 }, 906 }, 907 }, 908 } 909 } 910 911 // GithookService returns a k8s service that exposes a public IP 912 func GithookService(namespace string) *v1.Service { 913 name := "githook" 914 return &v1.Service{ 915 TypeMeta: metav1.TypeMeta{ 916 Kind: "Service", 917 APIVersion: "v1", 918 }, 919 ObjectMeta: objectMeta(name, labels(name), nil, namespace), 920 Spec: v1.ServiceSpec{ 921 Type: v1.ServiceTypeLoadBalancer, 922 Selector: map[string]string{ 923 "app": pachdName, 924 }, 925 Ports: []v1.ServicePort{ 926 { 927 TargetPort: intstr.FromInt(githook.GitHookPort), 928 Name: "api-git-port", 929 Port: githook.ExternalPort(), 930 }, 931 }, 932 }, 933 } 934 } 935 936 // EtcdDeployment returns an etcd k8s Deployment. 937 func EtcdDeployment(opts *AssetOpts, hostPath string) *apps.Deployment { 938 cpu := resource.MustParse(opts.EtcdCPURequest) 939 mem := resource.MustParse(opts.EtcdMemRequest) 940 var volumes []v1.Volume 941 if hostPath == "" { 942 volumes = []v1.Volume{ 943 { 944 Name: "etcd-storage", 945 VolumeSource: v1.VolumeSource{ 946 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 947 ClaimName: etcdVolumeClaimName, 948 }, 949 }, 950 }, 951 } 952 } else { 953 pathType := v1.HostPathDirectoryOrCreate 954 volumes = []v1.Volume{ 955 { 956 Name: "etcd-storage", 957 VolumeSource: v1.VolumeSource{ 958 HostPath: &v1.HostPathVolumeSource{ 959 Path: path.Join(hostPath, "etcd"), 960 Type: &pathType, 961 }, 962 }, 963 }, 964 } 965 } 966 resourceRequirements := v1.ResourceRequirements{ 967 Requests: v1.ResourceList{ 968 v1.ResourceCPU: cpu, 969 v1.ResourceMemory: mem, 970 }, 971 } 972 if !opts.NoGuaranteed { 973 resourceRequirements.Limits = v1.ResourceList{ 974 v1.ResourceCPU: cpu, 975 v1.ResourceMemory: mem, 976 } 977 } 978 // Don't want to strip the registry out of etcdImage since it's from quay 979 // not docker hub. 980 image := etcdImage 981 if opts.Registry != "" { 982 image = AddRegistry(opts.Registry, etcdImage) 983 } 984 return &apps.Deployment{ 985 TypeMeta: metav1.TypeMeta{ 986 Kind: "Deployment", 987 APIVersion: "apps/v1", 988 }, 989 ObjectMeta: objectMeta(etcdName, labels(etcdName), nil, opts.Namespace), 990 Spec: apps.DeploymentSpec{ 991 Replicas: replicas(1), 992 Selector: &metav1.LabelSelector{ 993 MatchLabels: labels(etcdName), 994 }, 995 Template: v1.PodTemplateSpec{ 996 ObjectMeta: objectMeta(etcdName, labels(etcdName), nil, opts.Namespace), 997 Spec: v1.PodSpec{ 998 Containers: []v1.Container{ 999 { 1000 Name: etcdName, 1001 Image: image, 1002 //TODO figure out how to get a cluster of these to talk to each other 1003 Command: etcdCmd, 1004 Ports: []v1.ContainerPort{ 1005 { 1006 ContainerPort: 2379, 1007 Name: "client-port", 1008 }, 1009 { 1010 ContainerPort: 2380, 1011 Name: "peer-port", 1012 }, 1013 }, 1014 VolumeMounts: []v1.VolumeMount{ 1015 { 1016 Name: "etcd-storage", 1017 MountPath: "/var/data/etcd", 1018 }, 1019 }, 1020 ImagePullPolicy: "IfNotPresent", 1021 Resources: resourceRequirements, 1022 }, 1023 }, 1024 Volumes: volumes, 1025 ImagePullSecrets: imagePullSecrets(opts), 1026 }, 1027 }, 1028 }, 1029 } 1030 } 1031 1032 // EtcdStorageClass creates a storage class used for dynamic volume 1033 // provisioning. Currently dynamic volume provisioning only works 1034 // on AWS and GCE. 1035 func EtcdStorageClass(opts *AssetOpts, backend Backend) (interface{}, error) { 1036 return makeStorageClass(opts, backend, defaultEtcdStorageClassName, labels(etcdName)) 1037 } 1038 1039 // PostgresStorageClass creates a storage class used for dynamic volume 1040 // provisioning. Currently dynamic volume provisioning only works 1041 // on AWS and GCE. 1042 func PostgresStorageClass(opts *AssetOpts, backend Backend) (interface{}, error) { 1043 return makeStorageClass(opts, backend, defaultPostgresStorageClassName, labels(postgresName)) 1044 } 1045 1046 func makeStorageClass( 1047 opts *AssetOpts, 1048 backend Backend, 1049 storageClassName string, 1050 storageClassLabels map[string]string, 1051 ) (interface{}, error) { 1052 sc := map[string]interface{}{ 1053 "apiVersion": "storage.k8s.io/v1", 1054 "kind": "StorageClass", 1055 "metadata": map[string]interface{}{ 1056 "name": storageClassName, 1057 "labels": storageClassLabels, 1058 "namespace": opts.Namespace, 1059 }, 1060 "allowVolumeExpansion": true, 1061 } 1062 switch backend { 1063 case GoogleBackend: 1064 sc["provisioner"] = "kubernetes.io/gce-pd" 1065 sc["parameters"] = map[string]string{ 1066 "type": "pd-ssd", 1067 } 1068 case AmazonBackend: 1069 sc["provisioner"] = "kubernetes.io/aws-ebs" 1070 sc["parameters"] = map[string]string{ 1071 "type": "gp2", 1072 } 1073 default: 1074 return nil, nil 1075 } 1076 return sc, nil 1077 } 1078 1079 // EtcdVolume creates a persistent volume backed by a volume with name "name" 1080 func EtcdVolume(persistentDiskBackend Backend, opts *AssetOpts, 1081 hostPath string, name string, size int) (*v1.PersistentVolume, error) { 1082 return makePersistentVolume(persistentDiskBackend, opts, hostPath, name, size, etcdVolumeName, labels(etcdName)) 1083 } 1084 1085 // PostgresVolume creates a persistent volume backed by a volume with name "name" 1086 func PostgresVolume(persistentDiskBackend Backend, opts *AssetOpts, 1087 hostPath string, name string, size int) (*v1.PersistentVolume, error) { 1088 return makePersistentVolume(persistentDiskBackend, opts, hostPath, name, size, postgresVolumeName, labels(postgresName)) 1089 } 1090 1091 func makePersistentVolume( 1092 persistentDiskBackend Backend, 1093 opts *AssetOpts, 1094 hostPath string, 1095 name string, 1096 size int, 1097 volumeName string, 1098 volumeLabels map[string]string, 1099 ) (*v1.PersistentVolume, error) { 1100 spec := &v1.PersistentVolume{ 1101 TypeMeta: metav1.TypeMeta{ 1102 Kind: "PersistentVolume", 1103 APIVersion: "v1", 1104 }, 1105 ObjectMeta: objectMeta(volumeName, volumeLabels, nil, opts.Namespace), 1106 Spec: v1.PersistentVolumeSpec{ 1107 Capacity: map[v1.ResourceName]resource.Quantity{ 1108 "storage": resource.MustParse(fmt.Sprintf("%vGi", size)), 1109 }, 1110 AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 1111 PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimRetain, 1112 }, 1113 } 1114 1115 switch persistentDiskBackend { 1116 case AmazonBackend: 1117 spec.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 1118 AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{ 1119 FSType: "ext4", 1120 VolumeID: name, 1121 }, 1122 } 1123 case GoogleBackend: 1124 spec.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 1125 GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ 1126 FSType: "ext4", 1127 PDName: name, 1128 }, 1129 } 1130 case MicrosoftBackend: 1131 dataDiskURI := name 1132 split := strings.Split(name, "/") 1133 diskName := split[len(split)-1] 1134 1135 spec.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 1136 AzureDisk: &v1.AzureDiskVolumeSource{ 1137 DiskName: diskName, 1138 DataDiskURI: dataDiskURI, 1139 }, 1140 } 1141 case MinioBackend: 1142 fallthrough 1143 case LocalBackend: 1144 pathType := v1.HostPathDirectoryOrCreate 1145 spec.Spec.PersistentVolumeSource = v1.PersistentVolumeSource{ 1146 HostPath: &v1.HostPathVolumeSource{ 1147 Path: path.Join(hostPath, volumeName), 1148 Type: &pathType, 1149 }, 1150 } 1151 default: 1152 return nil, errors.Errorf("cannot generate volume spec for unknown backend \"%v\"", persistentDiskBackend) 1153 } 1154 return spec, nil 1155 } 1156 1157 // EtcdVolumeClaim creates a persistent volume claim of 'size' GB. 1158 // 1159 // Note that if you're controlling Etcd with a Stateful Set, this is 1160 // unnecessary (the stateful set controller will create PVCs automatically). 1161 func EtcdVolumeClaim(size int, opts *AssetOpts) *v1.PersistentVolumeClaim { 1162 return makeVolumeClaim(size, opts, etcdVolumeName, etcdVolumeClaimName, labels(etcdName)) 1163 } 1164 1165 // PostgresVolumeClaim creates a persistent volume claim of 'size' GB. 1166 // 1167 // Note that if you're controlling Postgres with a Stateful Set, this is 1168 // unnecessary (the stateful set controller will create PVCs automatically). 1169 func PostgresVolumeClaim(size int, opts *AssetOpts) *v1.PersistentVolumeClaim { 1170 return makeVolumeClaim(size, opts, postgresVolumeName, postgresVolumeClaimName, labels(postgresName)) 1171 } 1172 1173 func makeVolumeClaim( 1174 size int, 1175 opts *AssetOpts, 1176 volumeName string, 1177 volumeClaimName string, 1178 volumeClaimLabels map[string]string, 1179 ) *v1.PersistentVolumeClaim { 1180 return &v1.PersistentVolumeClaim{ 1181 TypeMeta: metav1.TypeMeta{ 1182 Kind: "PersistentVolumeClaim", 1183 APIVersion: "v1", 1184 }, 1185 ObjectMeta: objectMeta(volumeClaimName, volumeClaimLabels, nil, opts.Namespace), 1186 Spec: v1.PersistentVolumeClaimSpec{ 1187 Resources: v1.ResourceRequirements{ 1188 Requests: map[v1.ResourceName]resource.Quantity{ 1189 "storage": resource.MustParse(fmt.Sprintf("%vGi", size)), 1190 }, 1191 }, 1192 AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, 1193 VolumeName: volumeName, 1194 }, 1195 } 1196 } 1197 1198 // EtcdNodePortService returns a NodePort etcd service. This will let non-etcd 1199 // pods talk to etcd 1200 func EtcdNodePortService(local bool, opts *AssetOpts) *v1.Service { 1201 var clientNodePort int32 1202 if local { 1203 clientNodePort = 32379 1204 } 1205 return &v1.Service{ 1206 TypeMeta: metav1.TypeMeta{ 1207 Kind: "Service", 1208 APIVersion: "v1", 1209 }, 1210 ObjectMeta: objectMeta(etcdName, labels(etcdName), nil, opts.Namespace), 1211 Spec: v1.ServiceSpec{ 1212 Type: v1.ServiceTypeNodePort, 1213 Selector: map[string]string{ 1214 "app": etcdName, 1215 }, 1216 Ports: []v1.ServicePort{ 1217 { 1218 Port: 2379, 1219 Name: "client-port", 1220 NodePort: clientNodePort, 1221 }, 1222 }, 1223 }, 1224 } 1225 } 1226 1227 // EtcdHeadlessService returns a headless etcd service, which is only for DNS 1228 // resolution. 1229 func EtcdHeadlessService(opts *AssetOpts) *v1.Service { 1230 return &v1.Service{ 1231 TypeMeta: metav1.TypeMeta{ 1232 Kind: "Service", 1233 APIVersion: "v1", 1234 }, 1235 ObjectMeta: objectMeta(etcdHeadlessServiceName, labels(etcdName), nil, opts.Namespace), 1236 Spec: v1.ServiceSpec{ 1237 Selector: map[string]string{ 1238 "app": etcdName, 1239 }, 1240 ClusterIP: "None", 1241 Ports: []v1.ServicePort{ 1242 { 1243 Name: "peer-port", 1244 Port: 2380, 1245 }, 1246 }, 1247 }, 1248 } 1249 } 1250 1251 // EtcdStatefulSet returns a stateful set that manages an etcd cluster 1252 func EtcdStatefulSet(opts *AssetOpts, backend Backend, diskSpace int) interface{} { 1253 mem := resource.MustParse(opts.EtcdMemRequest) 1254 cpu := resource.MustParse(opts.EtcdCPURequest) 1255 initialCluster := make([]string, 0, opts.EtcdNodes) 1256 for i := 0; i < opts.EtcdNodes; i++ { 1257 url := fmt.Sprintf("http://etcd-%d.etcd-headless.${NAMESPACE}.svc.cluster.local:2380", i) 1258 initialCluster = append(initialCluster, fmt.Sprintf("etcd-%d=%s", i, url)) 1259 } 1260 // Because we need to refer to some environment variables set the by the 1261 // k8s downward API, we define the command for running etcd here, and then 1262 // actually run it below via '/bin/sh -c ${CMD}' 1263 etcdCmd := append(etcdCmd, 1264 "--listen-peer-urls=http://0.0.0.0:2380", 1265 "--initial-cluster-token=pach-cluster", // unique ID 1266 "--initial-advertise-peer-urls=http://${ETCD_NAME}.etcd-headless.${NAMESPACE}.svc.cluster.local:2380", 1267 "--initial-cluster="+strings.Join(initialCluster, ","), 1268 ) 1269 for i, str := range etcdCmd { 1270 etcdCmd[i] = fmt.Sprintf("\"%s\"", str) // quote all arguments, for shell 1271 } 1272 1273 var pvcTemplates []interface{} 1274 switch backend { 1275 case GoogleBackend, AmazonBackend: 1276 storageClassName := opts.EtcdStorageClassName 1277 if storageClassName == "" { 1278 storageClassName = defaultEtcdStorageClassName 1279 } 1280 pvcTemplates = []interface{}{ 1281 map[string]interface{}{ 1282 "metadata": map[string]interface{}{ 1283 "name": etcdVolumeClaimName, 1284 "labels": labels(etcdName), 1285 "annotations": map[string]string{ 1286 "volume.beta.kubernetes.io/storage-class": storageClassName, 1287 }, 1288 "namespace": opts.Namespace, 1289 }, 1290 "spec": map[string]interface{}{ 1291 "resources": map[string]interface{}{ 1292 "requests": map[string]interface{}{ 1293 "storage": resource.MustParse(fmt.Sprintf("%vGi", diskSpace)), 1294 }, 1295 }, 1296 "accessModes": []string{"ReadWriteOnce"}, 1297 }, 1298 }, 1299 } 1300 default: 1301 pvcTemplates = []interface{}{ 1302 map[string]interface{}{ 1303 "metadata": map[string]interface{}{ 1304 "name": etcdVolumeClaimName, 1305 "labels": labels(etcdName), 1306 "namespace": opts.Namespace, 1307 }, 1308 "spec": map[string]interface{}{ 1309 "resources": map[string]interface{}{ 1310 "requests": map[string]interface{}{ 1311 "storage": resource.MustParse(fmt.Sprintf("%vGi", diskSpace)), 1312 }, 1313 }, 1314 "accessModes": []string{"ReadWriteOnce"}, 1315 }, 1316 }, 1317 } 1318 } 1319 var imagePullSecrets []map[string]string 1320 if opts.ImagePullSecret != "" { 1321 imagePullSecrets = append(imagePullSecrets, map[string]string{"name": opts.ImagePullSecret}) 1322 } 1323 // As of March 17, 2017, the Kubernetes client does not include structs for 1324 // Stateful Set, so we generate the kubernetes manifest using raw json. 1325 // TODO(msteffen): we're now upgrading our kubernetes client, so we should be 1326 // abe to rewrite this spec using k8s client structs 1327 image := etcdImage 1328 if opts.Registry != "" { 1329 image = AddRegistry(opts.Registry, etcdImage) 1330 } 1331 return map[string]interface{}{ 1332 "apiVersion": "apps/v1", 1333 "kind": "StatefulSet", 1334 "metadata": map[string]interface{}{ 1335 "name": etcdName, 1336 "labels": labels(etcdName), 1337 "namespace": opts.Namespace, 1338 }, 1339 "spec": map[string]interface{}{ 1340 // Effectively configures a RC 1341 "serviceName": etcdHeadlessServiceName, 1342 "replicas": int(opts.EtcdNodes), 1343 "selector": map[string]interface{}{ 1344 "matchLabels": labels(etcdName), 1345 }, 1346 1347 // pod template 1348 "template": map[string]interface{}{ 1349 "metadata": map[string]interface{}{ 1350 "name": etcdName, 1351 "labels": labels(etcdName), 1352 "namespace": opts.Namespace, 1353 }, 1354 "spec": map[string]interface{}{ 1355 "imagePullSecrets": imagePullSecrets, 1356 "containers": []interface{}{ 1357 map[string]interface{}{ 1358 "name": etcdName, 1359 "image": image, 1360 "command": []string{"/bin/sh", "-c"}, 1361 "args": []string{strings.Join(etcdCmd, " ")}, 1362 // Use the downward API to pass the pod name to etcd. This sets 1363 // the etcd-internal name of each node to its pod name. 1364 "env": []map[string]interface{}{{ 1365 "name": "ETCD_NAME", 1366 "valueFrom": map[string]interface{}{ 1367 "fieldRef": map[string]interface{}{ 1368 "apiVersion": "v1", 1369 "fieldPath": "metadata.name", 1370 }, 1371 }, 1372 }, { 1373 "name": "NAMESPACE", 1374 "valueFrom": map[string]interface{}{ 1375 "fieldRef": map[string]interface{}{ 1376 "apiVersion": "v1", 1377 "fieldPath": "metadata.namespace", 1378 }, 1379 }, 1380 }}, 1381 "ports": []interface{}{ 1382 map[string]interface{}{ 1383 "containerPort": 2379, 1384 "name": "client-port", 1385 }, 1386 map[string]interface{}{ 1387 "containerPort": 2380, 1388 "name": "peer-port", 1389 }, 1390 }, 1391 "volumeMounts": []interface{}{ 1392 map[string]interface{}{ 1393 "name": etcdVolumeClaimName, 1394 "mountPath": "/var/data/etcd", 1395 }, 1396 }, 1397 "imagePullPolicy": "IfNotPresent", 1398 "resources": map[string]interface{}{ 1399 "requests": map[string]interface{}{ 1400 string(v1.ResourceCPU): cpu.String(), 1401 string(v1.ResourceMemory): mem.String(), 1402 }, 1403 }, 1404 }, 1405 }, 1406 }, 1407 }, 1408 "volumeClaimTemplates": pvcTemplates, 1409 }, 1410 } 1411 } 1412 1413 // DashDeployment creates a Deployment for the pachyderm dashboard. 1414 func DashDeployment(opts *AssetOpts) *apps.Deployment { 1415 return &apps.Deployment{ 1416 TypeMeta: metav1.TypeMeta{ 1417 Kind: "Deployment", 1418 APIVersion: "apps/v1", 1419 }, 1420 ObjectMeta: objectMeta(dashName, labels(dashName), nil, opts.Namespace), 1421 Spec: apps.DeploymentSpec{ 1422 Selector: &metav1.LabelSelector{ 1423 MatchLabels: labels(dashName), 1424 }, 1425 Template: v1.PodTemplateSpec{ 1426 ObjectMeta: objectMeta(dashName, labels(dashName), nil, opts.Namespace), 1427 Spec: v1.PodSpec{ 1428 Containers: []v1.Container{ 1429 { 1430 Name: dashName, 1431 Image: AddRegistry(opts.Registry, opts.DashImage), 1432 Ports: []v1.ContainerPort{ 1433 { 1434 ContainerPort: 8080, 1435 Name: "dash-http", 1436 }, 1437 }, 1438 ImagePullPolicy: "IfNotPresent", 1439 }, 1440 { 1441 Name: grpcProxyName, 1442 Image: AddRegistry(opts.Registry, grpcProxyImage), 1443 Ports: []v1.ContainerPort{ 1444 { 1445 ContainerPort: 8081, 1446 Name: "grpc-proxy-http", 1447 }, 1448 }, 1449 ImagePullPolicy: "IfNotPresent", 1450 }, 1451 }, 1452 ImagePullSecrets: imagePullSecrets(opts), 1453 }, 1454 }, 1455 }, 1456 } 1457 } 1458 1459 // DashService creates a Service for the pachyderm dashboard. 1460 func DashService(opts *AssetOpts) *v1.Service { 1461 return &v1.Service{ 1462 TypeMeta: metav1.TypeMeta{ 1463 Kind: "Service", 1464 APIVersion: "v1", 1465 }, 1466 ObjectMeta: objectMeta(dashName, labels(dashName), nil, opts.Namespace), 1467 Spec: v1.ServiceSpec{ 1468 Type: v1.ServiceTypeNodePort, 1469 Selector: labels(dashName), 1470 Ports: []v1.ServicePort{ 1471 { 1472 Port: 8080, 1473 Name: "dash-http", 1474 NodePort: 30080, 1475 }, 1476 { 1477 Port: 8081, 1478 Name: "grpc-proxy-http", 1479 NodePort: 30081, 1480 }, 1481 }, 1482 }, 1483 } 1484 } 1485 1486 // PostgresDeployment generates a Deployment for the pachyderm postgres instance. 1487 func PostgresDeployment(opts *AssetOpts, hostPath string) *apps.Deployment { 1488 cpu := resource.MustParse(opts.PostgresCPURequest) 1489 mem := resource.MustParse(opts.PostgresMemRequest) 1490 var volumes []v1.Volume 1491 if hostPath == "" { 1492 volumes = []v1.Volume{ 1493 { 1494 Name: "postgres-storage", 1495 VolumeSource: v1.VolumeSource{ 1496 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 1497 ClaimName: postgresVolumeClaimName, 1498 }, 1499 }, 1500 }, 1501 } 1502 } else { 1503 volumes = []v1.Volume{ 1504 { 1505 Name: "postgres-storage", 1506 VolumeSource: v1.VolumeSource{ 1507 HostPath: &v1.HostPathVolumeSource{ 1508 Path: filepath.Join(hostPath, "postgres"), 1509 }, 1510 }, 1511 }, 1512 } 1513 } 1514 resourceRequirements := v1.ResourceRequirements{ 1515 Requests: v1.ResourceList{ 1516 v1.ResourceCPU: cpu, 1517 v1.ResourceMemory: mem, 1518 }, 1519 } 1520 if !opts.NoGuaranteed { 1521 resourceRequirements.Limits = v1.ResourceList{ 1522 v1.ResourceCPU: cpu, 1523 v1.ResourceMemory: mem, 1524 } 1525 } 1526 image := postgresImage 1527 if opts.Registry != "" { 1528 image = AddRegistry(opts.Registry, postgresImage) 1529 } 1530 return &apps.Deployment{ 1531 TypeMeta: metav1.TypeMeta{ 1532 Kind: "Deployment", 1533 APIVersion: "apps/v1beta1", 1534 }, 1535 ObjectMeta: objectMeta(postgresName, labels(postgresName), nil, opts.Namespace), 1536 Spec: apps.DeploymentSpec{ 1537 Replicas: replicas(1), 1538 Selector: &metav1.LabelSelector{ 1539 MatchLabels: labels(postgresName), 1540 }, 1541 Template: v1.PodTemplateSpec{ 1542 ObjectMeta: objectMeta(postgresName, labels(postgresName), nil, opts.Namespace), 1543 Spec: v1.PodSpec{ 1544 Containers: []v1.Container{ 1545 { 1546 Name: postgresName, 1547 Image: image, 1548 //TODO figure out how to get a cluster of these to talk to each other 1549 Ports: []v1.ContainerPort{ 1550 { 1551 ContainerPort: 5432, 1552 Name: "client-port", 1553 }, 1554 }, 1555 VolumeMounts: []v1.VolumeMount{ 1556 { 1557 Name: "postgres-storage", 1558 MountPath: "/var/lib/postgresql/data", 1559 }, 1560 }, 1561 ImagePullPolicy: "IfNotPresent", 1562 Resources: resourceRequirements, 1563 Env: []v1.EnvVar{ 1564 {Name: "POSTGRES_DB", Value: "pgc"}, 1565 {Name: "POSTGRES_USER", Value: "pachyderm"}, 1566 {Name: "POSTGRES_PASSWORD", Value: "elephantastic"}, 1567 }, 1568 }, 1569 }, 1570 Volumes: volumes, 1571 ImagePullSecrets: imagePullSecrets(opts), 1572 }, 1573 }, 1574 }, 1575 } 1576 } 1577 1578 // PostgresService generates a Service for the pachyderm postgres instance. 1579 func PostgresService(local bool, opts *AssetOpts) *v1.Service { 1580 var clientNodePort int32 1581 if local { 1582 clientNodePort = 32228 1583 } 1584 return &v1.Service{ 1585 TypeMeta: metav1.TypeMeta{ 1586 Kind: "Service", 1587 APIVersion: "v1", 1588 }, 1589 ObjectMeta: objectMeta(postgresName, labels(postgresName), nil, opts.Namespace), 1590 Spec: v1.ServiceSpec{ 1591 Type: v1.ServiceTypeNodePort, 1592 Selector: map[string]string{ 1593 "app": postgresName, 1594 }, 1595 Ports: []v1.ServicePort{ 1596 { 1597 Port: 5432, 1598 Name: "client-port", 1599 NodePort: clientNodePort, 1600 }, 1601 }, 1602 }, 1603 } 1604 } 1605 1606 // MinioSecret creates an amazon secret with the following parameters: 1607 // bucket - S3 bucket name 1608 // id - S3 access key id 1609 // secret - S3 secret access key 1610 // endpoint - S3 compatible endpoint 1611 // secure - set to true for a secure connection. 1612 // isS3V2 - Set to true if client follows S3V2 1613 func MinioSecret(bucket string, id string, secret string, endpoint string, secure, isS3V2 bool) map[string][]byte { 1614 secureV := "0" 1615 if secure { 1616 secureV = "1" 1617 } 1618 s3V2 := "0" 1619 if isS3V2 { 1620 s3V2 = "1" 1621 } 1622 return map[string][]byte{ 1623 "minio-bucket": []byte(bucket), 1624 "minio-id": []byte(id), 1625 "minio-secret": []byte(secret), 1626 "minio-endpoint": []byte(endpoint), 1627 "minio-secure": []byte(secureV), 1628 "minio-signature": []byte(s3V2), 1629 } 1630 } 1631 1632 // WriteSecret writes a JSON-encoded k8s secret to the given writer. 1633 // The secret uses the given map as data. 1634 func WriteSecret(encoder serde.Encoder, data map[string][]byte, opts *AssetOpts) error { 1635 if opts.DashOnly { 1636 return nil 1637 } 1638 secret := &v1.Secret{ 1639 TypeMeta: metav1.TypeMeta{ 1640 Kind: "Secret", 1641 APIVersion: "v1", 1642 }, 1643 ObjectMeta: objectMeta(client.StorageSecretName, labels(client.StorageSecretName), nil, opts.Namespace), 1644 Data: data, 1645 } 1646 return encoder.Encode(secret) 1647 } 1648 1649 // LocalSecret creates an empty secret. 1650 func LocalSecret() map[string][]byte { 1651 return nil 1652 } 1653 1654 // AmazonSecret creates an amazon secret with the following parameters: 1655 // region - AWS region 1656 // bucket - S3 bucket name 1657 // id - AWS access key id 1658 // secret - AWS secret access key 1659 // token - AWS access token 1660 // distribution - cloudfront distribution 1661 // endpoint - Custom endpoint (generally used for S3 compatible object stores) 1662 // advancedConfig - advanced configuration 1663 func AmazonSecret(region, bucket, id, secret, token, distribution, endpoint string, advancedConfig *obj.AmazonAdvancedConfiguration) map[string][]byte { 1664 s := amazonBasicSecret(region, bucket, distribution, advancedConfig) 1665 s["amazon-id"] = []byte(id) 1666 s["amazon-secret"] = []byte(secret) 1667 s["amazon-token"] = []byte(token) 1668 s["custom-endpoint"] = []byte(endpoint) 1669 return s 1670 } 1671 1672 // AmazonVaultSecret creates an amazon secret with the following parameters: 1673 // region - AWS region 1674 // bucket - S3 bucket name 1675 // vaultAddress - address/hostport of vault 1676 // vaultRole - pachd's role in vault 1677 // vaultToken - pachd's vault token 1678 // distribution - cloudfront distribution 1679 // advancedConfig - advanced configuration 1680 func AmazonVaultSecret(region, bucket, vaultAddress, vaultRole, vaultToken, distribution string, advancedConfig *obj.AmazonAdvancedConfiguration) map[string][]byte { 1681 s := amazonBasicSecret(region, bucket, distribution, advancedConfig) 1682 s["amazon-vault-addr"] = []byte(vaultAddress) 1683 s["amazon-vault-role"] = []byte(vaultRole) 1684 s["amazon-vault-token"] = []byte(vaultToken) 1685 return s 1686 } 1687 1688 // AmazonIAMRoleSecret creates an amazon secret with the following parameters: 1689 // region - AWS region 1690 // bucket - S3 bucket name 1691 // distribution - cloudfront distribution 1692 // advancedConfig - advanced configuration 1693 func AmazonIAMRoleSecret(region, bucket, distribution string, advancedConfig *obj.AmazonAdvancedConfiguration) map[string][]byte { 1694 return amazonBasicSecret(region, bucket, distribution, advancedConfig) 1695 } 1696 1697 func amazonBasicSecret(region, bucket, distribution string, advancedConfig *obj.AmazonAdvancedConfiguration) map[string][]byte { 1698 return map[string][]byte{ 1699 "amazon-region": []byte(region), 1700 "amazon-bucket": []byte(bucket), 1701 "amazon-distribution": []byte(distribution), 1702 "retries": []byte(strconv.Itoa(advancedConfig.Retries)), 1703 "timeout": []byte(advancedConfig.Timeout), 1704 "upload-acl": []byte(advancedConfig.UploadACL), 1705 "reverse": []byte(strconv.FormatBool(advancedConfig.Reverse)), 1706 "part-size": []byte(strconv.FormatInt(advancedConfig.PartSize, 10)), 1707 "max-upload-parts": []byte(strconv.Itoa(advancedConfig.MaxUploadParts)), 1708 "disable-ssl": []byte(strconv.FormatBool(advancedConfig.DisableSSL)), 1709 "no-verify-ssl": []byte(strconv.FormatBool(advancedConfig.NoVerifySSL)), 1710 "log-options": []byte(advancedConfig.LogOptions), 1711 } 1712 } 1713 1714 // GoogleSecret creates a google secret with a bucket name. 1715 func GoogleSecret(bucket string, cred string) map[string][]byte { 1716 return map[string][]byte{ 1717 "google-bucket": []byte(bucket), 1718 "google-cred": []byte(cred), 1719 } 1720 } 1721 1722 // MicrosoftSecret creates a microsoft secret with following parameters: 1723 // container - Azure blob container 1724 // id - Azure storage account name 1725 // secret - Azure storage account key 1726 func MicrosoftSecret(container string, id string, secret string) map[string][]byte { 1727 return map[string][]byte{ 1728 "microsoft-container": []byte(container), 1729 "microsoft-id": []byte(id), 1730 "microsoft-secret": []byte(secret), 1731 } 1732 } 1733 1734 // WriteDashboardAssets writes the k8s config for deploying the Pachyderm 1735 // dashboard to 'encoder' 1736 func WriteDashboardAssets(encoder serde.Encoder, opts *AssetOpts) error { 1737 if err := encoder.Encode(DashService(opts)); err != nil { 1738 return err 1739 } 1740 return encoder.Encode(DashDeployment(opts)) 1741 } 1742 1743 // WriteAssets writes the assets to encoder. 1744 func WriteAssets(encoder serde.Encoder, opts *AssetOpts, objectStoreBackend Backend, 1745 persistentDiskBackend Backend, volumeSize int, 1746 hostPath string) error { 1747 fillDefaultResourceRequests(opts, persistentDiskBackend) 1748 if opts.DashOnly { 1749 if dashErr := WriteDashboardAssets(encoder, opts); dashErr != nil { 1750 return dashErr 1751 } 1752 return nil 1753 } 1754 1755 for _, sa := range ServiceAccounts(opts) { 1756 if err := encoder.Encode(sa); err != nil { 1757 return err 1758 } 1759 } 1760 if !opts.NoRBAC { 1761 if opts.LocalRoles { 1762 if err := encoder.Encode(Role(opts)); err != nil { 1763 return err 1764 } 1765 if err := encoder.Encode(RoleBinding(opts)); err != nil { 1766 return err 1767 } 1768 } else { 1769 if err := encoder.Encode(ClusterRole(opts)); err != nil { 1770 return err 1771 } 1772 if err := encoder.Encode(ClusterRoleBinding(opts)); err != nil { 1773 return err 1774 } 1775 } 1776 if err := encoder.Encode(workerRole(opts)); err != nil { 1777 return err 1778 } 1779 if err := encoder.Encode(workerRoleBinding(opts)); err != nil { 1780 return err 1781 } 1782 } 1783 1784 if opts.EtcdNodes > 0 && opts.EtcdVolume != "" { 1785 return errors.Errorf("only one of --dynamic-etcd-nodes and --static-etcd-volume should be given, but not both") 1786 } 1787 1788 // In the dynamic route, we create a storage class which dynamically 1789 // provisions volumes, and run etcd as a stateful set. 1790 // In the static route, we create a single volume, a single volume 1791 // claim, and run etcd as a replication controller with a single node. 1792 if persistentDiskBackend == LocalBackend { 1793 if err := encoder.Encode(EtcdDeployment(opts, hostPath)); err != nil { 1794 return err 1795 } 1796 } else if opts.EtcdNodes > 0 { 1797 // Create a StorageClass, if the user didn't provide one. 1798 if opts.EtcdStorageClassName == "" { 1799 sc, err := EtcdStorageClass(opts, persistentDiskBackend) 1800 if err != nil { 1801 return err 1802 } 1803 if sc != nil { 1804 if err = encoder.Encode(sc); err != nil { 1805 return err 1806 } 1807 } 1808 } 1809 if err := encoder.Encode(EtcdHeadlessService(opts)); err != nil { 1810 return err 1811 } 1812 if err := encoder.Encode(EtcdStatefulSet(opts, persistentDiskBackend, volumeSize)); err != nil { 1813 return err 1814 } 1815 } else if opts.EtcdVolume != "" { 1816 volume, err := EtcdVolume(persistentDiskBackend, opts, hostPath, opts.EtcdVolume, volumeSize) 1817 if err != nil { 1818 return err 1819 } 1820 if err = encoder.Encode(volume); err != nil { 1821 return err 1822 } 1823 if err = encoder.Encode(EtcdVolumeClaim(volumeSize, opts)); err != nil { 1824 return err 1825 } 1826 if err = encoder.Encode(EtcdDeployment(opts, "")); err != nil { 1827 return err 1828 } 1829 } else { 1830 return errors.Errorf("unless deploying locally, either --dynamic-etcd-nodes or --static-etcd-volume needs to be provided") 1831 } 1832 if err := encoder.Encode(EtcdNodePortService(persistentDiskBackend == LocalBackend, opts)); err != nil { 1833 return err 1834 } 1835 1836 if opts.StorageV2 { 1837 // In the dynamic route, we create a storage class which dynamically 1838 // provisions volumes, and run postgres as a stateful set. 1839 // In the static route, we create a single volume, a single volume 1840 // claim, and run etcd as a replication controller with a single node. 1841 if persistentDiskBackend == LocalBackend { 1842 if err := encoder.Encode(PostgresDeployment(opts, hostPath)); err != nil { 1843 return err 1844 } 1845 } else if opts.PostgresNodes > 0 { 1846 // Create a StorageClass, if the user didn't provide one. 1847 if opts.PostgresStorageClassName == "" { 1848 sc, err := PostgresStorageClass(opts, persistentDiskBackend) 1849 if err != nil { 1850 return err 1851 } 1852 if sc != nil { 1853 if err = encoder.Encode(sc); err != nil { 1854 return err 1855 } 1856 } 1857 } 1858 // TODO: is this necessary? 1859 // if err := encoder.Encode(PostgresHeadlessService(opts)); err != nil { 1860 // return err 1861 // } 1862 // TODO: add stateful set 1863 // if err := encoder.Encode(PostgresStatefulSet(opts, persistentDiskBackend, volumeSize)); err != nil { 1864 // return err 1865 // } 1866 } else if opts.PostgresVolume != "" { 1867 volume, err := PostgresVolume(persistentDiskBackend, opts, hostPath, opts.PostgresVolume, volumeSize) 1868 if err != nil { 1869 return err 1870 } 1871 if err = encoder.Encode(volume); err != nil { 1872 return err 1873 } 1874 if err = encoder.Encode(PostgresVolumeClaim(volumeSize, opts)); err != nil { 1875 return err 1876 } 1877 if err = encoder.Encode(PostgresDeployment(opts, "")); err != nil { 1878 return err 1879 } 1880 } else { 1881 return fmt.Errorf("unless deploying locally, either --dynamic-etcd-nodes or --static-etcd-volume needs to be provided") 1882 } 1883 if err := encoder.Encode(PostgresService(persistentDiskBackend == LocalBackend, opts)); err != nil { 1884 return err 1885 } 1886 } 1887 1888 if err := encoder.Encode(PachdService(opts)); err != nil { 1889 return err 1890 } 1891 if err := encoder.Encode(PachdPeerService(opts)); err != nil { 1892 return err 1893 } 1894 if err := encoder.Encode(PachdDeployment(opts, objectStoreBackend, hostPath)); err != nil { 1895 return err 1896 } 1897 if !opts.NoDash { 1898 if err := WriteDashboardAssets(encoder, opts); err != nil { 1899 return err 1900 } 1901 } 1902 if opts.TLS != nil { 1903 if err := WriteTLSSecret(encoder, opts); err != nil { 1904 return err 1905 } 1906 } 1907 return nil 1908 } 1909 1910 // WriteTLSSecret creates a new TLS secret in the kubernetes manifest 1911 // (equivalent to one generate by 'kubectl create secret tls'). This will be 1912 // mounted by the pachd pod and used as its TLS public certificate and private 1913 // key 1914 func WriteTLSSecret(encoder serde.Encoder, opts *AssetOpts) error { 1915 // Validate arguments 1916 if opts.DashOnly { 1917 return nil 1918 } 1919 if opts.TLS == nil { 1920 return errors.Errorf("internal error: WriteTLSSecret called but opts.TLS is nil") 1921 } 1922 if opts.TLS.ServerKey == "" { 1923 return errors.Errorf("internal error: WriteTLSSecret called but opts.TLS.ServerKey is \"\"") 1924 } 1925 if opts.TLS.ServerCert == "" { 1926 return errors.Errorf("internal error: WriteTLSSecret called but opts.TLS.ServerCert is \"\"") 1927 } 1928 1929 // Attempt to copy server cert and key files into config (kubernetes client 1930 // does the base64-encoding) 1931 certBytes, err := ioutil.ReadFile(opts.TLS.ServerCert) 1932 if err != nil { 1933 return errors.Wrapf(err, "could not open server cert at \"%s\"", opts.TLS.ServerCert) 1934 } 1935 keyBytes, err := ioutil.ReadFile(opts.TLS.ServerKey) 1936 if err != nil { 1937 return errors.Wrapf(err, "could not open server key at \"%s\"", opts.TLS.ServerKey) 1938 } 1939 secret := &v1.Secret{ 1940 TypeMeta: metav1.TypeMeta{ 1941 Kind: "Secret", 1942 APIVersion: "v1", 1943 }, 1944 ObjectMeta: objectMeta(tlsSecretName, labels(tlsSecretName), nil, opts.Namespace), 1945 Data: map[string][]byte{ 1946 tls.CertFile: certBytes, 1947 tls.KeyFile: keyBytes, 1948 }, 1949 } 1950 return encoder.Encode(secret) 1951 } 1952 1953 // WriteLocalAssets writes assets to a local backend. 1954 func WriteLocalAssets(encoder serde.Encoder, opts *AssetOpts, hostPath string) error { 1955 if err := WriteAssets(encoder, opts, LocalBackend, LocalBackend, 1 /* = volume size (gb) */, hostPath); err != nil { 1956 return err 1957 } 1958 if secretErr := WriteSecret(encoder, LocalSecret(), opts); secretErr != nil { 1959 return secretErr 1960 } 1961 return nil 1962 } 1963 1964 // WriteCustomAssets writes assets to a custom combination of object-store and persistent disk. 1965 func WriteCustomAssets(encoder serde.Encoder, opts *AssetOpts, args []string, objectStoreBackend string, 1966 persistentDiskBackend string, secure, isS3V2 bool, advancedConfig *obj.AmazonAdvancedConfiguration) error { 1967 switch objectStoreBackend { 1968 case "s3": 1969 if len(args) != S3CustomArgs { 1970 return errors.Errorf("expected %d arguments for disk+s3 backend", S3CustomArgs) 1971 } 1972 volumeSize, err := strconv.Atoi(args[1]) 1973 if err != nil { 1974 return errors.Errorf("volume size needs to be an integer; instead got %v", args[1]) 1975 } 1976 objectStoreBackend := AmazonBackend 1977 // (bryce) use minio if we need v2 signing enabled. 1978 if isS3V2 { 1979 objectStoreBackend = MinioBackend 1980 } 1981 switch persistentDiskBackend { 1982 case "aws": 1983 if err := WriteAssets(encoder, opts, objectStoreBackend, AmazonBackend, volumeSize, ""); err != nil { 1984 return err 1985 } 1986 case "google": 1987 if err := WriteAssets(encoder, opts, objectStoreBackend, GoogleBackend, volumeSize, ""); err != nil { 1988 return err 1989 } 1990 case "azure": 1991 if err := WriteAssets(encoder, opts, objectStoreBackend, MicrosoftBackend, volumeSize, ""); err != nil { 1992 return err 1993 } 1994 default: 1995 return errors.Errorf("did not recognize the choice of persistent-disk") 1996 } 1997 bucket := args[2] 1998 id := args[3] 1999 secret := args[4] 2000 endpoint := args[5] 2001 if objectStoreBackend == MinioBackend { 2002 return WriteSecret(encoder, MinioSecret(bucket, id, secret, endpoint, secure, isS3V2), opts) 2003 } 2004 // (bryce) hardcode region? 2005 return WriteSecret(encoder, AmazonSecret("us-east-1", bucket, id, secret, "", "", endpoint, advancedConfig), opts) 2006 default: 2007 return errors.Errorf("did not recognize the choice of object-store") 2008 } 2009 } 2010 2011 // AmazonCreds are options that are applicable specifically to Pachd's 2012 // credentials in an AWS deployment 2013 type AmazonCreds struct { 2014 // Direct credentials. Only applicable if Pachyderm is given its own permanent 2015 // AWS credentials 2016 ID string // Access Key ID 2017 Secret string // Secret Access Key 2018 Token string // Access token (if using temporary security credentials 2019 2020 // Vault options (if getting AWS credentials from Vault) 2021 VaultAddress string // normally addresses come from env, but don't have vault service name 2022 VaultRole string 2023 VaultToken string 2024 } 2025 2026 // WriteAmazonAssets writes assets to an amazon backend. 2027 func WriteAmazonAssets(encoder serde.Encoder, opts *AssetOpts, region string, bucket string, volumeSize int, creds *AmazonCreds, cloudfrontDistro string, advancedConfig *obj.AmazonAdvancedConfiguration) error { 2028 if err := WriteAssets(encoder, opts, AmazonBackend, AmazonBackend, volumeSize, ""); err != nil { 2029 return err 2030 } 2031 var secret map[string][]byte 2032 if creds == nil { 2033 secret = AmazonIAMRoleSecret(region, bucket, cloudfrontDistro, advancedConfig) 2034 } else if creds.ID != "" { 2035 secret = AmazonSecret(region, bucket, creds.ID, creds.Secret, creds.Token, cloudfrontDistro, "", advancedConfig) 2036 } else if creds.VaultAddress != "" { 2037 secret = AmazonVaultSecret(region, bucket, creds.VaultAddress, creds.VaultRole, creds.VaultToken, cloudfrontDistro, advancedConfig) 2038 } 2039 return WriteSecret(encoder, secret, opts) 2040 } 2041 2042 // WriteGoogleAssets writes assets to a google backend. 2043 func WriteGoogleAssets(encoder serde.Encoder, opts *AssetOpts, bucket string, cred string, volumeSize int) error { 2044 if err := WriteAssets(encoder, opts, GoogleBackend, GoogleBackend, volumeSize, ""); err != nil { 2045 return err 2046 } 2047 return WriteSecret(encoder, GoogleSecret(bucket, cred), opts) 2048 } 2049 2050 // WriteMicrosoftAssets writes assets to a microsoft backend 2051 func WriteMicrosoftAssets(encoder serde.Encoder, opts *AssetOpts, container string, id string, secret string, volumeSize int) error { 2052 if err := WriteAssets(encoder, opts, MicrosoftBackend, MicrosoftBackend, volumeSize, ""); err != nil { 2053 return err 2054 } 2055 return WriteSecret(encoder, MicrosoftSecret(container, id, secret), opts) 2056 } 2057 2058 // Images returns a list of all the images that are used by a pachyderm deployment. 2059 func Images(opts *AssetOpts) []string { 2060 return []string{ 2061 versionedWorkerImage(opts), 2062 etcdImage, 2063 postgresImage, 2064 grpcProxyImage, 2065 pauseImage, 2066 versionedPachdImage(opts), 2067 opts.DashImage, 2068 } 2069 } 2070 2071 func labels(name string) map[string]string { 2072 return map[string]string{ 2073 "app": name, 2074 "suite": suite, 2075 } 2076 } 2077 2078 func objectMeta(name string, labels, annotations map[string]string, namespace string) metav1.ObjectMeta { 2079 return metav1.ObjectMeta{ 2080 Name: name, 2081 Labels: labels, 2082 Annotations: annotations, 2083 Namespace: namespace, 2084 } 2085 } 2086 2087 // AddRegistry switches the registry that an image is targeting, unless registry is blank 2088 func AddRegistry(registry string, imageName string) string { 2089 if registry == "" { 2090 return imageName 2091 } 2092 parts := strings.Split(imageName, "/") 2093 if len(parts) == 3 { 2094 parts = parts[1:] 2095 } 2096 return path.Join(registry, parts[0], parts[1]) 2097 }