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  }