github.com/docker/compose-on-kubernetes@v0.5.0/install/install.go (about) 1 package install 2 3 import ( 4 "context" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "os" 9 "strconv" 10 "time" 11 12 kubeapiclientset "github.com/docker/compose-on-kubernetes/api/client/clientset" 13 apiv1beta2 "github.com/docker/compose-on-kubernetes/api/compose/v1beta2" 14 "github.com/docker/compose-on-kubernetes/internal/e2e/wait" 15 log "github.com/sirupsen/logrus" 16 appsv1types "k8s.io/api/apps/v1" 17 corev1types "k8s.io/api/core/v1" 18 v1 "k8s.io/api/core/v1" 19 rbacv1types "k8s.io/api/rbac/v1" 20 apierrors "k8s.io/apimachinery/pkg/api/errors" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/util/intstr" 23 "k8s.io/client-go/kubernetes" 24 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 25 "k8s.io/client-go/rest" 26 ) 27 28 const ( 29 // TimeoutDefault is the default install timeout. 30 TimeoutDefault = 30 * time.Second 31 32 // installWaitNumMaxPolls is the maximum number of API operations to be 33 // performed in sequence while waiting for the component to be installed. 34 installWaitNumMaxPolls = 60 35 36 fryKey = "com.docker.fry" 37 imageTagKey = "com.docker.image-tag" 38 namespaceKey = "com.docker.deploy-namespace" 39 defaultServiceTypeKey = "com.docker.default-service-type" 40 customTLSHashAnnotationName = "com.docker.custom-tls-hash" 41 composeFry = "compose" 42 composeAPIServerFry = "compose.api" 43 composeGroupName = "compose.docker.com" 44 45 controllerDebugPort = 40000 46 apiServerDebugPort = 40001 47 ) 48 49 var ( 50 imageRepoPrefix = "docker/kube-compose-" 51 imagePrefix = func() string { 52 if ir := os.Getenv("IMAGE_REPO_PREFIX"); ir != "" { 53 return ir 54 } 55 return imageRepoPrefix 56 }() 57 everythingSelector = fmt.Sprintf("%s in (%s, %s)", fryKey, composeFry, composeAPIServerFry) 58 ) 59 60 var linuxAmd64NodeAffinity = &corev1types.Affinity{ 61 NodeAffinity: &corev1types.NodeAffinity{ 62 RequiredDuringSchedulingIgnoredDuringExecution: &corev1types.NodeSelector{ 63 NodeSelectorTerms: []corev1types.NodeSelectorTerm{ 64 { 65 MatchExpressions: []corev1types.NodeSelectorRequirement{ 66 { 67 Key: "beta.kubernetes.io/os", 68 Operator: corev1types.NodeSelectorOpIn, 69 Values: []string{"linux"}, 70 }, 71 { 72 Key: "beta.kubernetes.io/arch", 73 Operator: corev1types.NodeSelectorOpIn, 74 Values: []string{"amd64"}, 75 }, 76 }, 77 }, 78 }, 79 }, 80 }, 81 } 82 83 // GetInstallStatus retrives the current installation status 84 func GetInstallStatus(config *rest.Config) (Status, error) { 85 installer, err := newInstaller(config) 86 if err != nil { 87 return Status{}, err 88 } 89 return installer.isInstalled() 90 } 91 92 // Unsafe installs the Compose features without High availability, and with insecure ETCD. 93 func Unsafe(ctx context.Context, config *rest.Config, options UnsafeOptions) error { 94 return Do(ctx, config, WithUnsafe(options)) 95 } 96 97 // WaitNPods waits for n pods to be up 98 func WaitNPods(config *rest.Config, namespace string, count int, timeout time.Duration) error { 99 log.Infof("Wait for %d pod(s) to be up with timeout %s", count, timeout) 100 client, err := corev1.NewForConfig(config) 101 if err != nil { 102 return err 103 } 104 105 period := 2 * time.Second 106 for start := time.Now(); time.Since(start) < timeout; time.Sleep(period) { 107 log.Debugf("Check pod(s) are running...") 108 pods, err := client.Pods(namespace).List(metav1.ListOptions{ 109 LabelSelector: everythingSelector, 110 }) 111 if err != nil { 112 return err 113 } 114 115 if len(pods.Items) != count { 116 log.Debugf("Pod(s) not yet created, waiting %s", period) 117 continue 118 } 119 120 running, err := allRunning(pods.Items) 121 if err != nil { 122 return err 123 } 124 125 if running { 126 return nil 127 } 128 log.Debugf("Pod(s) not running, waiting %s", period) 129 } 130 131 return errors.New("installation timed out") 132 } 133 134 func checkPodContainers(pod corev1types.Pod) error { 135 for _, status := range pod.Status.ContainerStatuses { 136 waiting := status.State.Waiting 137 if waiting != nil { 138 if IsErrImagePull(waiting.Reason) { 139 return errors.New(waiting.Message) 140 } 141 } 142 } 143 return nil 144 } 145 146 func allRunning(pods []corev1types.Pod) (bool, error) { 147 for _, pod := range pods { 148 switch pod.Status.Phase { 149 case corev1types.PodRunning: 150 case corev1types.PodPending: 151 return false, checkPodContainers(pod) 152 case corev1types.PodFailed: 153 return false, errors.New("unable to start controller: " + pod.Status.Message) 154 default: 155 return false, nil 156 } 157 } 158 return true, nil 159 } 160 161 // IsRunning checks if the compose api server is available 162 func IsRunning(config *rest.Config) (bool, error) { 163 client, err := kubernetes.NewForConfig(config) 164 if err != nil { 165 return false, err 166 } 167 168 groups, err := client.Discovery().ServerGroups() 169 if err != nil { 170 return false, err 171 } 172 173 for _, group := range groups.Groups { 174 if group.Name == apiv1beta2.SchemeGroupVersion.Group { 175 stackClient, err := kubeapiclientset.NewForConfig(config) 176 if err != nil { 177 return false, err 178 } 179 err = wait.For(installWaitNumMaxPolls, func() (bool, error) { 180 _, err := stackClient.ComposeV1beta2().Stacks("e2e").List(metav1.ListOptions{}) 181 if err != nil { 182 return false, nil 183 } 184 _, err = stackClient.ComposeV1beta1().Stacks("e2e").List(metav1.ListOptions{}) 185 if err != nil { 186 return false, nil 187 } 188 _, err = stackClient.ComposeV1alpha3().Stacks("e2e").List(metav1.ListOptions{}) 189 return err == nil, nil 190 }) 191 return err == nil, err 192 } 193 } 194 return false, nil 195 } 196 197 func (c *installer) createNamespace(*installerContext) error { 198 log.Debugf("Create namespace: %s", c.commonOptions.Namespace) 199 200 if _, err := c.coreClient.Namespaces().Get(c.commonOptions.Namespace, metav1.GetOptions{}); err == nil { 201 return nil 202 } 203 ns := &corev1types.Namespace{ 204 ObjectMeta: metav1.ObjectMeta{ 205 Name: c.commonOptions.Namespace, 206 }, 207 } 208 shouldDo, err := c.objectFilter.filter(ns) 209 if err != nil { 210 return err 211 } 212 if shouldDo { 213 _, err := c.coreClient.Namespaces().Create(ns) 214 return err 215 } 216 return nil 217 } 218 219 func (c *installer) createPullSecretIfRequired(ctx *installerContext) error { 220 if c.commonOptions.PullSecret == "" { 221 return nil 222 } 223 log.Debug("Create pull secret") 224 secret, err := c.coreClient.Secrets(c.commonOptions.Namespace).Get("compose", metav1.GetOptions{}) 225 if err == nil { 226 ctx.pullSecret = secret 227 return nil 228 } 229 230 bin, err := base64.StdEncoding.DecodeString(c.commonOptions.PullSecret) 231 if err != nil { 232 return err 233 } 234 235 secret = &corev1types.Secret{ 236 ObjectMeta: metav1.ObjectMeta{ 237 Name: "compose", 238 Namespace: c.commonOptions.Namespace, 239 Labels: c.labels, 240 }, 241 Data: map[string][]byte{ 242 ".dockercfg": bin, 243 }, 244 Type: corev1types.SecretTypeDockercfg, 245 } 246 shouldDo, err := c.objectFilter.filter(secret) 247 if err != nil { 248 return err 249 } 250 if shouldDo { 251 secret, err = c.coreClient.Secrets(c.commonOptions.Namespace).Create(secret) 252 } 253 ctx.pullSecret = secret 254 return err 255 } 256 257 func (c *installer) createServiceAccount(ctx *installerContext) error { 258 log.Debug("Create ServiceAccount") 259 sa, err := c.coreClient.ServiceAccounts(c.commonOptions.Namespace).Get("compose", metav1.GetOptions{}) 260 if err == nil { 261 ctx.serviceAccount = sa 262 return nil 263 } 264 sa = &corev1types.ServiceAccount{ 265 ObjectMeta: metav1.ObjectMeta{ 266 Name: "compose", 267 Namespace: c.commonOptions.Namespace, 268 Labels: c.labels, 269 }, 270 } 271 shouldDo, err := c.objectFilter.filter(sa) 272 if err != nil { 273 return err 274 } 275 if shouldDo { 276 sa, err = c.coreClient.ServiceAccounts(c.commonOptions.Namespace).Create(sa) 277 } 278 ctx.serviceAccount = sa 279 return err 280 } 281 282 var composeRoleRules = []rbacv1types.PolicyRule{ 283 { 284 APIGroups: []string{""}, 285 Resources: []string{"users", "groups", "serviceaccounts"}, 286 Verbs: []string{"impersonate"}, 287 }, 288 { 289 APIGroups: []string{"authentication.k8s.io"}, 290 Resources: []string{"*"}, 291 Verbs: []string{"impersonate"}, 292 }, 293 { 294 APIGroups: []string{"", "apps"}, 295 Resources: []string{"services", "deployments", "statefulsets", "daemonsets"}, 296 Verbs: []string{"get"}, 297 }, 298 { 299 APIGroups: []string{""}, 300 Resources: []string{"pods", "pods/log"}, 301 Verbs: []string{"get", "watch", "list"}, 302 }, 303 { 304 APIGroups: []string{composeGroupName}, 305 Resources: []string{"stacks"}, 306 Verbs: []string{"*"}, 307 }, 308 { 309 APIGroups: []string{composeGroupName}, 310 Resources: []string{"stacks/owner"}, 311 Verbs: []string{"get"}, 312 }, 313 { 314 APIGroups: []string{"admissionregistration.k8s.io"}, 315 Resources: []string{"validatingwebhookconfigurations", "mutatingwebhookconfigurations"}, 316 Verbs: []string{"get", "watch", "list"}, 317 }, 318 { 319 APIGroups: []string{"apiregistration.k8s.io"}, 320 Resources: []string{"apiservices"}, 321 ResourceNames: []string{"v1beta1.compose.docker.com", "v1beta2.compose.docker.com", "v1alpha3.compose.docker.com"}, 322 Verbs: []string{"*"}, 323 }, 324 { 325 APIGroups: []string{"apiregistration.k8s.io"}, 326 Resources: []string{"apiservices"}, 327 Verbs: []string{"create"}, 328 }, 329 } 330 331 func viewStackRole() *rbacv1types.ClusterRole { 332 return &rbacv1types.ClusterRole{ 333 ObjectMeta: metav1.ObjectMeta{ 334 Name: "compose-stack-view", 335 Labels: map[string]string{ 336 "rbac.authorization.k8s.io/aggregate-to-view": "true", 337 }, 338 }, 339 Rules: []rbacv1types.PolicyRule{ 340 { 341 APIGroups: []string{composeGroupName}, 342 Resources: []string{"stacks", "stacks/scale", "stacks/log", "stacks/composeFile"}, 343 Verbs: []string{"get", "list", "watch"}, 344 }, 345 }, 346 } 347 } 348 349 func editStackRole() *rbacv1types.ClusterRole { 350 return &rbacv1types.ClusterRole{ 351 ObjectMeta: metav1.ObjectMeta{ 352 Name: "compose-stack-edit", 353 Labels: map[string]string{ 354 "rbac.authorization.k8s.io/aggregate-to-edit": "true", 355 }, 356 }, 357 Rules: []rbacv1types.PolicyRule{ 358 { 359 APIGroups: []string{composeGroupName}, 360 Resources: []string{"stacks", "stacks/scale", "stacks/log", "stacks/composeFile"}, 361 Verbs: []string{ 362 "create", 363 "delete", 364 "deletecollection", 365 "get", 366 "list", 367 "patch", 368 "update", 369 "watch", 370 }, 371 }, 372 }, 373 } 374 } 375 376 func adminStackRole() *rbacv1types.ClusterRole { 377 return &rbacv1types.ClusterRole{ 378 ObjectMeta: metav1.ObjectMeta{ 379 Name: "compose-stack-admin", 380 Labels: map[string]string{ 381 "rbac.authorization.k8s.io/aggregate-to-admin": "true", 382 }, 383 }, 384 Rules: []rbacv1types.PolicyRule{ 385 { 386 APIGroups: []string{composeGroupName}, 387 Resources: []string{"stacks", "stacks/scale", "stacks/log", "stacks/composeFile"}, 388 Verbs: []string{ 389 "create", 390 "delete", 391 "deletecollection", 392 "get", 393 "list", 394 "patch", 395 "update", 396 "watch", 397 }, 398 }, 399 { 400 APIGroups: []string{composeGroupName}, 401 Resources: []string{"stacks/owner"}, 402 Verbs: []string{"get"}, 403 }, 404 }, 405 } 406 } 407 408 func (c *installer) createDefaultClusterRoles(_ *installerContext) error { 409 var shouldDo bool 410 roles := []*rbacv1types.ClusterRole{viewStackRole(), editStackRole(), adminStackRole()} 411 for _, r := range roles { 412 existing, err := c.rbacClient.ClusterRoles().Get(r.Name, metav1.GetOptions{}) 413 if apierrors.IsNotFound(err) { 414 if shouldDo, err = c.objectFilter.filter(r); err != nil { 415 return err 416 } 417 if shouldDo { 418 if _, err := c.rbacClient.ClusterRoles().Create(r); err != nil { 419 return err 420 } 421 } 422 } else if err != nil { 423 return err 424 } else { 425 r.ResourceVersion = existing.ResourceVersion 426 if shouldDo, err = c.objectFilter.filter(r); err != nil { 427 return err 428 } 429 if shouldDo { 430 if _, err := c.rbacClient.ClusterRoles().Update(r); err != nil { 431 return err 432 } 433 } 434 } 435 } 436 return nil 437 } 438 439 func (c *installer) createSAClusterRole() error { 440 role, err := c.rbacClient.ClusterRoles().Get("compose-service", metav1.GetOptions{}) 441 var shouldDo bool 442 if apierrors.IsNotFound(err) { 443 role = &rbacv1types.ClusterRole{ 444 ObjectMeta: metav1.ObjectMeta{ 445 Name: "compose-service", 446 Labels: c.labels, 447 }, 448 Rules: composeRoleRules, 449 } 450 if shouldDo, err = c.objectFilter.filter(role); err != nil { 451 return err 452 } 453 if shouldDo { 454 role, err = c.rbacClient.ClusterRoles().Create(role) 455 } 456 } else if err == nil { 457 role.Rules = composeRoleRules 458 if shouldDo, err = c.objectFilter.filter(role); err != nil { 459 return err 460 } 461 if shouldDo { 462 role, err = c.rbacClient.ClusterRoles().Update(role) 463 } 464 } 465 return err 466 } 467 468 type roleBindingRequirement struct { 469 name string 470 namespace string 471 roleRef rbacv1types.RoleRef 472 } 473 474 var requiredRoleBindings = []roleBindingRequirement{ 475 { 476 name: "compose-auth-reader", 477 namespace: "kube-system", 478 roleRef: rbacv1types.RoleRef{ 479 APIGroup: "rbac.authorization.k8s.io", 480 Kind: "Role", 481 Name: "extension-apiserver-authentication-reader", 482 }, 483 }, 484 } 485 var requiredClusteRoleBindings = []roleBindingRequirement{ 486 { 487 name: "compose-auth-delegator", 488 roleRef: rbacv1types.RoleRef{ 489 APIGroup: "rbac.authorization.k8s.io", 490 Kind: "ClusterRole", 491 Name: "system:auth-delegator", 492 }, 493 }, 494 { 495 name: "compose-auth-view", 496 roleRef: rbacv1types.RoleRef{ 497 APIGroup: "rbac.authorization.k8s.io", 498 Kind: "ClusterRole", 499 Name: "view", 500 }, 501 }, 502 { 503 name: "compose", 504 roleRef: rbacv1types.RoleRef{ 505 APIGroup: "rbac.authorization.k8s.io", 506 Kind: "ClusterRole", 507 Name: "compose-service", 508 }, 509 }, 510 } 511 512 func (c *installer) createSARoleBindings(ctx *installerContext) error { 513 subjects := []rbacv1types.Subject{ 514 { 515 Kind: "ServiceAccount", 516 Name: ctx.serviceAccount.Name, 517 Namespace: ctx.serviceAccount.Namespace, 518 }, 519 } 520 var shouldDo bool 521 for _, req := range requiredRoleBindings { 522 shouldCreate := false 523 rb, err := c.rbacClient.RoleBindings(req.namespace).Get(req.name, metav1.GetOptions{}) 524 if apierrors.IsNotFound(err) { 525 shouldCreate = true 526 rb = &rbacv1types.RoleBinding{ 527 ObjectMeta: metav1.ObjectMeta{ 528 Name: req.name, 529 Labels: c.labels, 530 Namespace: req.namespace, 531 }, 532 RoleRef: req.roleRef, 533 Subjects: subjects, 534 } 535 } else if err == nil { 536 rb.RoleRef = req.roleRef 537 rb.Subjects = subjects 538 } 539 if shouldDo, err = c.objectFilter.filter(rb); err != nil { 540 return err 541 } 542 if shouldDo { 543 if shouldCreate { 544 _, err = c.rbacClient.RoleBindings(req.namespace).Create(rb) 545 } else { 546 _, err = c.rbacClient.RoleBindings(req.namespace).Update(rb) 547 } 548 } 549 if err != nil { 550 return err 551 } 552 } 553 for _, req := range requiredClusteRoleBindings { 554 shouldCreate := false 555 crb, err := c.rbacClient.ClusterRoleBindings().Get(req.name, metav1.GetOptions{}) 556 if apierrors.IsNotFound(err) { 557 shouldCreate = true 558 crb = &rbacv1types.ClusterRoleBinding{ 559 ObjectMeta: metav1.ObjectMeta{ 560 Name: req.name, 561 Labels: c.labels, 562 }, 563 RoleRef: req.roleRef, 564 Subjects: subjects, 565 } 566 } else if err == nil { 567 crb.RoleRef = req.roleRef 568 crb.Subjects = subjects 569 } 570 if shouldDo, err = c.objectFilter.filter(crb); err != nil { 571 return err 572 } 573 if shouldDo { 574 if shouldCreate { 575 _, err = c.rbacClient.ClusterRoleBindings().Create(crb) 576 } else { 577 _, err = c.rbacClient.ClusterRoleBindings().Update(crb) 578 } 579 } 580 if err != nil { 581 return err 582 } 583 } 584 585 return nil 586 } 587 588 func (c *installer) createClusterRoleBindings(ctx *installerContext) error { 589 log.Debug("Create stack cluster role bindings") 590 if err := c.createSAClusterRole(); err != nil { 591 return err 592 } 593 594 log.Debug("Create auth RoleBindings") 595 596 return c.createSARoleBindings(ctx) 597 } 598 599 func applyCustomTLSHash(hash string, deploy *appsv1types.Deployment) { 600 if hash == "" { 601 return 602 } 603 if deploy.Annotations == nil { 604 deploy.Annotations = make(map[string]string) 605 } 606 if deploy.Spec.Template.Annotations == nil { 607 deploy.Spec.Template.Annotations = make(map[string]string) 608 } 609 deploy.Annotations[customTLSHashAnnotationName] = hash 610 deploy.Spec.Template.Annotations[customTLSHashAnnotationName] = hash 611 } 612 613 func (c *installer) configureAPIServerImage() (string, []string, []corev1types.EnvVar, corev1types.PullPolicy) { 614 if c.enableCoverage { 615 return imagePrefix + "api-server-coverage" + ":" + c.commonOptions.Tag, 616 []string{}, 617 []corev1types.EnvVar{{Name: "TEST_API_SERVER", Value: "1"}}, 618 corev1types.PullNever 619 } 620 args := []string{ 621 "--kubeconfig", "", 622 "--authentication-kubeconfig=", 623 "--authorization-kubeconfig=", 624 "--service-name=compose-api", 625 "--service-namespace", c.commonOptions.Namespace, 626 "--healthz-check-port", strconv.Itoa(c.commonOptions.HealthzCheckPort), 627 } 628 if c.debugImages { 629 return imagePrefix + "api-server-debug:latest", 630 args, 631 []corev1types.EnvVar{}, 632 corev1types.PullNever 633 } 634 return imagePrefix + "api-server" + ":" + c.commonOptions.Tag, 635 args, 636 []corev1types.EnvVar{}, 637 c.commonOptions.PullPolicy 638 } 639 640 func (c *installer) validateOptions() error { 641 if c.etcdOptions == nil && c.commonOptions.APIServerReplicas != nil && *c.commonOptions.APIServerReplicas != 1 { 642 // etcdOptions == nil makes the installer run etcd as a sidecar container of the APIServer 643 // thus, the user cannot scale it 644 return errors.New("can't specify the API server replicas without referencing an external etcd instance") 645 } 646 return nil 647 } 648 649 func (c *installer) createAPIServer(ctx *installerContext) error { 650 log.Debugf("Create API server deployment and service in namespace %q", c.commonOptions.Namespace) 651 image, args, env, pullPolicy := c.configureAPIServerImage() 652 if c.apiServerImageOverride != "" { 653 image = c.apiServerImageOverride 654 } 655 656 affinity := c.commonOptions.APIServerAffinity 657 if affinity == nil { 658 affinity = linuxAmd64NodeAffinity 659 } 660 661 log.Infof("Api server: image: %q, pullPolicy: %q", image, pullPolicy) 662 663 deploy := &appsv1types.Deployment{ 664 ObjectMeta: metav1.ObjectMeta{ 665 Name: "compose-api", 666 Namespace: c.commonOptions.Namespace, 667 Labels: c.apiLabels, 668 }, 669 Spec: appsv1types.DeploymentSpec{ 670 Selector: &metav1.LabelSelector{ 671 MatchLabels: c.apiLabels, 672 }, 673 Template: corev1types.PodTemplateSpec{ 674 ObjectMeta: metav1.ObjectMeta{ 675 Labels: c.apiLabels, 676 }, 677 Spec: corev1types.PodSpec{ 678 ServiceAccountName: ctx.serviceAccount.Name, 679 ImagePullSecrets: pullSecrets(ctx.pullSecret), 680 Containers: []corev1types.Container{ 681 { 682 Name: "compose", 683 Image: image, 684 ImagePullPolicy: pullPolicy, 685 Args: args, 686 Env: env, 687 LivenessProbe: &corev1types.Probe{ 688 Handler: corev1types.Handler{ 689 HTTPGet: &corev1types.HTTPGetAction{ 690 Path: "/healthz", 691 Port: intstr.FromInt(c.commonOptions.HealthzCheckPort), 692 Scheme: corev1types.URISchemeHTTP, 693 }, 694 }, 695 InitialDelaySeconds: 15, 696 TimeoutSeconds: 15, 697 FailureThreshold: 8, 698 }, 699 }, 700 }, 701 Affinity: affinity, 702 }, 703 }, 704 Replicas: c.commonOptions.APIServerReplicas, 705 }, 706 } 707 if c.commonOptions.HealthzCheckPort == 0 { 708 deploy.Spec.Template.Spec.Containers[0].LivenessProbe = nil 709 } 710 711 applyEtcdOptions(&deploy.Spec.Template.Spec, c.etcdOptions) 712 applyNetworkOptions(&deploy.Spec.Template.Spec, c.networkOptions) 713 port := 9443 714 if c.networkOptions != nil && c.networkOptions.Port != 0 { 715 port = int(c.networkOptions.Port) 716 } 717 718 applyCustomTLSHash(c.customTLSHash, deploy) 719 720 shouldDo, err := c.objectFilter.filter(deploy) 721 if err != nil { 722 return err 723 } 724 if shouldDo { 725 if c.debugImages { 726 trueval := true 727 for ix := range deploy.Spec.Template.Spec.Containers { 728 deploy.Spec.Template.Spec.Containers[ix].SecurityContext = &corev1types.SecurityContext{ 729 Privileged: &trueval, 730 } 731 deploy.Spec.Template.Spec.Containers[ix].LivenessProbe = nil 732 } 733 } 734 d, err := c.appsClient.Deployments(c.commonOptions.Namespace).Get("compose-api", metav1.GetOptions{}) 735 if err == nil { 736 deploy.ObjectMeta.ResourceVersion = d.ObjectMeta.ResourceVersion 737 _, err = c.appsClient.Deployments(c.commonOptions.Namespace).Update(deploy) 738 } else { 739 _, err = c.appsClient.Deployments(c.commonOptions.Namespace).Create(deploy) 740 } 741 if err != nil { 742 return err 743 } 744 } 745 746 if err = c.createAPIServerService(port); err != nil { 747 return err 748 } 749 if c.debugImages { 750 // create a load balanced service for exposing remote debug endpoint 751 return c.createDebugService("compose-api-server-remote-debug", apiServerDebugPort, c.apiLabels) 752 } 753 return nil 754 } 755 756 func (c *installer) createAPIServerService(port int) error { 757 svc := &corev1types.Service{ 758 ObjectMeta: metav1.ObjectMeta{ 759 Name: "compose-api", 760 Namespace: c.commonOptions.Namespace, 761 Labels: c.apiLabels, 762 }, 763 Spec: corev1types.ServiceSpec{ 764 Ports: []corev1types.ServicePort{ 765 { 766 Name: "api", 767 Port: 443, 768 TargetPort: intstr.FromInt(port), 769 }, 770 }, 771 Selector: c.apiLabels, 772 }, 773 } 774 shouldDo, err := c.objectFilter.filter(svc) 775 if err != nil { 776 return err 777 } 778 if shouldDo { 779 s, err := c.coreClient.Services(c.commonOptions.Namespace).Get("compose-api", metav1.GetOptions{}) 780 if err == nil { 781 svc.Spec.ClusterIP = s.Spec.ClusterIP 782 svc.ObjectMeta.ResourceVersion = s.ObjectMeta.ResourceVersion 783 _, err = c.coreClient.Services(c.commonOptions.Namespace).Update(svc) 784 } else { 785 _, err = c.coreClient.Services(c.commonOptions.Namespace).Create(svc) 786 } 787 return err 788 } 789 return nil 790 } 791 792 func pullSecrets(secret *corev1types.Secret) []corev1types.LocalObjectReference { 793 if secret == nil { 794 return nil 795 } 796 return []corev1types.LocalObjectReference{{Name: secret.Name}} 797 } 798 799 func (c *installer) configureControllerImage() (string, []string, v1.PullPolicy) { 800 if c.enableCoverage { 801 return imagePrefix + "controller-coverage" + ":" + c.commonOptions.Tag, []string{}, corev1types.PullNever 802 } 803 args := []string{ 804 "--kubeconfig", "", 805 "--reconciliation-interval", c.commonOptions.ReconciliationInterval.String(), 806 "--healthz-check-port", strconv.Itoa(c.commonOptions.HealthzCheckPort), 807 } 808 if c.debugImages { 809 return imagePrefix + "controller-debug:latest", args, corev1types.PullNever 810 } 811 return imagePrefix + "controller:" + c.commonOptions.Tag, args, c.commonOptions.PullPolicy 812 } 813 814 func (c *installer) createController(ctx *installerContext) error { 815 log.Debugf("Create deployment with tag %q in namespace %q, reconciliation interval %s", c.commonOptions.Tag, c.commonOptions.Namespace, c.commonOptions.ReconciliationInterval) 816 817 image, args, pullPolicy := c.configureControllerImage() 818 819 if c.commonOptions.DefaultServiceType != "" { 820 args = append(args, "--default-service-type="+c.commonOptions.DefaultServiceType) 821 } 822 823 if c.controllerImageOverride != "" { 824 image = c.controllerImageOverride 825 } 826 affinity := c.commonOptions.ControllerAffinity 827 if affinity == nil { 828 affinity = linuxAmd64NodeAffinity 829 } 830 log.Infof("Controller: image: %q, pullPolicy: %q", image, pullPolicy) 831 deploy := &appsv1types.Deployment{ 832 ObjectMeta: metav1.ObjectMeta{ 833 Name: "compose", 834 Namespace: c.commonOptions.Namespace, 835 Labels: c.labels, 836 }, 837 Spec: appsv1types.DeploymentSpec{ 838 Selector: &metav1.LabelSelector{ 839 MatchLabels: c.labels, 840 }, 841 Template: corev1types.PodTemplateSpec{ 842 ObjectMeta: metav1.ObjectMeta{ 843 Labels: c.labels, 844 }, 845 Spec: corev1types.PodSpec{ 846 ServiceAccountName: ctx.serviceAccount.Name, 847 ImagePullSecrets: pullSecrets(ctx.pullSecret), 848 Containers: []corev1types.Container{ 849 { 850 Name: "compose", 851 Image: image, 852 ImagePullPolicy: pullPolicy, 853 Args: args, 854 LivenessProbe: &corev1types.Probe{ 855 Handler: corev1types.Handler{ 856 HTTPGet: &corev1types.HTTPGetAction{ 857 Path: "/healthz", 858 Scheme: corev1types.URISchemeHTTP, 859 Port: intstr.FromInt(c.commonOptions.HealthzCheckPort), 860 }, 861 }, 862 InitialDelaySeconds: 15, 863 TimeoutSeconds: 15, 864 FailureThreshold: 8, 865 }, 866 }, 867 }, 868 Affinity: affinity, 869 }, 870 }, 871 }, 872 } 873 if c.enableCoverage { 874 envList := []corev1types.EnvVar{{Name: "TEST_COMPOSE_CONTROLLER", Value: "1"}} 875 if c.commonOptions.HealthzCheckPort > 0 { 876 envList = append(envList, corev1types.EnvVar{Name: "TEST_COMPOSE_HEALTHZ_PORT", Value: strconv.Itoa(c.commonOptions.HealthzCheckPort)}) 877 } 878 deploy.Spec.Template.Spec.Containers[0].Env = envList 879 } 880 881 if c.commonOptions.HealthzCheckPort == 0 { 882 deploy.Spec.Template.Spec.Containers[0].LivenessProbe = nil 883 } 884 885 shouldDo, err := c.objectFilter.filter(deploy) 886 if err != nil { 887 return err 888 } 889 if shouldDo { 890 if c.debugImages { 891 trueval := true 892 for ix := range deploy.Spec.Template.Spec.Containers { 893 deploy.Spec.Template.Spec.Containers[ix].SecurityContext = &corev1types.SecurityContext{ 894 Privileged: &trueval, 895 } 896 deploy.Spec.Template.Spec.Containers[ix].LivenessProbe = nil 897 } 898 } 899 d, err := c.appsClient.Deployments(c.commonOptions.Namespace).Get("compose", metav1.GetOptions{}) 900 if err == nil { 901 deploy.ObjectMeta.ResourceVersion = d.ObjectMeta.ResourceVersion 902 if _, err = c.appsClient.Deployments(c.commonOptions.Namespace).Update(deploy); err != nil { 903 return err 904 } 905 } else if _, err = c.appsClient.Deployments(c.commonOptions.Namespace).Create(deploy); err != nil { 906 return err 907 } 908 } 909 if c.debugImages { 910 // create a load balanced service for exposing remote debug endpoint 911 return c.createDebugService("compose-controller-remote-debug", controllerDebugPort, c.labels) 912 } 913 return nil 914 } 915 916 func (c *installer) createDebugService(name string, port int32, labels map[string]string) error { 917 svc, err := c.coreClient.Services(c.commonOptions.Namespace).Get(name, metav1.GetOptions{}) 918 if err == nil { 919 svc.Spec.Type = corev1types.ServiceTypeLoadBalancer 920 svc.Spec.Ports = []corev1types.ServicePort{ 921 {Name: "delve", Port: port, TargetPort: intstr.FromInt(40000)}, 922 } 923 svc.Spec.Selector = labels 924 _, err = c.coreClient.Services(c.commonOptions.Namespace).Update(svc) 925 return err 926 } 927 _, err = c.coreClient.Services(c.commonOptions.Namespace).Create(&corev1types.Service{ 928 ObjectMeta: metav1.ObjectMeta{ 929 Name: name, 930 Labels: labels, 931 }, 932 Spec: corev1types.ServiceSpec{ 933 Type: corev1types.ServiceTypeLoadBalancer, 934 Selector: labels, 935 Ports: []corev1types.ServicePort{ 936 {Name: "delve", Port: port, TargetPort: intstr.FromInt(40000)}, 937 }, 938 }, 939 }) 940 return err 941 }