github.com/docker/compose-on-kubernetes@v0.5.0/install/installer.go (about)

     1  package install
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha1"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"time"
     9  
    10  	log "github.com/sirupsen/logrus"
    11  	corev1types "k8s.io/api/core/v1"
    12  	apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
    13  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  	"k8s.io/client-go/discovery"
    17  	appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
    18  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    19  	rbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1"
    20  	"k8s.io/client-go/rest"
    21  	kubeaggreagatorv1beta1 "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1"
    22  )
    23  
    24  type installer struct {
    25  	coreClient              corev1.CoreV1Interface
    26  	rbacClient              rbacv1.RbacV1Interface
    27  	appsClient              appsv1.AppsV1Interface
    28  	aggregatorClient        kubeaggreagatorv1beta1.ApiregistrationV1beta1Interface
    29  	commonOptions           OptionsCommon
    30  	etcdOptions             *EtcdOptions
    31  	networkOptions          *NetworkOptions
    32  	enableCoverage          bool
    33  	config                  *rest.Config
    34  	labels                  map[string]string
    35  	apiLabels               map[string]string
    36  	disableController       bool
    37  	controllerOnly          bool
    38  	controllerImageOverride string
    39  	apiServerImageOverride  string
    40  	objectFilter            runtimeObjectFilters
    41  	customMatch             func(Status) bool
    42  	expiresOffset           time.Duration
    43  	customTLSHash           string
    44  	debugImages             bool
    45  }
    46  
    47  // RuntimeObjectFilter allows to modify or bypass completely a k8s object
    48  type RuntimeObjectFilter func(runtime.Object) (bool, error)
    49  
    50  type runtimeObjectFilters []RuntimeObjectFilter
    51  
    52  func (fs runtimeObjectFilters) filter(obj runtime.Object) (bool, error) {
    53  	for _, f := range fs {
    54  		if res, err := f(obj); err != nil || !res {
    55  			return res, err
    56  		}
    57  	}
    58  	return true, nil
    59  }
    60  
    61  type installerContext struct {
    62  	pullSecret     *corev1types.Secret
    63  	serviceAccount *corev1types.ServiceAccount
    64  }
    65  
    66  // Do proceeds with installing
    67  func Do(ctx context.Context, config *rest.Config, options ...InstallerOption) error {
    68  	installer, err := newInstaller(config, options...)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	return installer.install(ctx)
    73  }
    74  
    75  // InstallerOption defines modifies the installer
    76  type InstallerOption func(*installer)
    77  
    78  // WithObjectFilter applies a RuntimeObjectFilter
    79  func WithObjectFilter(filter RuntimeObjectFilter) InstallerOption {
    80  	return func(i *installer) {
    81  		i.objectFilter = append(i.objectFilter, filter)
    82  	}
    83  }
    84  
    85  // WithExpiresOffset specifies the duration offset to apply when checking if generated tls bundle has expired
    86  func WithExpiresOffset(d time.Duration) InstallerOption {
    87  	return func(i *installer) {
    88  		i.expiresOffset = d
    89  	}
    90  }
    91  
    92  // WithoutController install components without the controller
    93  func WithoutController() InstallerOption {
    94  	return func(i *installer) {
    95  		i.disableController = true
    96  	}
    97  }
    98  
    99  // WithControllerImage overrides controller image selection
   100  func WithControllerImage(image string) InstallerOption {
   101  	return func(i *installer) {
   102  		i.controllerImageOverride = image
   103  	}
   104  }
   105  
   106  // WithAPIServerImage overrides API server image selection
   107  func WithAPIServerImage(image string) InstallerOption {
   108  	return func(i *installer) {
   109  		i.apiServerImageOverride = image
   110  	}
   111  }
   112  
   113  // WithControllerOnly installs only the controller
   114  func WithControllerOnly() InstallerOption {
   115  	return func(i *installer) {
   116  		i.controllerOnly = true
   117  	}
   118  }
   119  
   120  // WithUnsafe initializes the installer with unsafe options
   121  func WithUnsafe(o UnsafeOptions) InstallerOption {
   122  	return func(i *installer) {
   123  		i.commonOptions = o.OptionsCommon
   124  		i.enableCoverage = o.Coverage
   125  		i.debugImages = o.Debug
   126  	}
   127  }
   128  
   129  // WithSafe initializes the installer with Safe options
   130  func WithSafe(o SafeOptions) InstallerOption {
   131  	return func(i *installer) {
   132  		i.commonOptions = o.OptionsCommon
   133  		i.etcdOptions = &o.Etcd
   134  		i.networkOptions = &o.Network
   135  	}
   136  }
   137  
   138  // WithCustomStatusMatch allows to provide additional predicates to
   139  // check if the current install status matches the desired state
   140  func WithCustomStatusMatch(match func(Status) bool) InstallerOption {
   141  	return func(i *installer) {
   142  		i.customMatch = match
   143  	}
   144  }
   145  
   146  func tagForCustomImages(controllerImage, apiServerImage string) string {
   147  	bytes := sha1.Sum([]byte(fmt.Sprintf("%s %s", controllerImage, apiServerImage)))
   148  	return hex.EncodeToString(bytes[:])
   149  }
   150  
   151  func newInstaller(config *rest.Config, options ...InstallerOption) (*installer, error) {
   152  	i := &installer{
   153  		config: config,
   154  	}
   155  	// default expires offset is 30 days
   156  	i.expiresOffset = 30 * 24 * time.Hour
   157  	for _, o := range options {
   158  		o(i)
   159  	}
   160  	if i.controllerImageOverride != "" && i.apiServerImageOverride != "" {
   161  		// compute a tag for these images
   162  		i.commonOptions.Tag = tagForCustomImages(i.controllerImageOverride, i.apiServerImageOverride)
   163  	}
   164  	if i.debugImages {
   165  		i.commonOptions.Tag = "debug"
   166  	}
   167  	i.labels = map[string]string{
   168  		fryKey:                composeFry,
   169  		imageTagKey:           i.commonOptions.Tag,
   170  		namespaceKey:          i.commonOptions.Namespace,
   171  		defaultServiceTypeKey: i.commonOptions.DefaultServiceType,
   172  	}
   173  	i.apiLabels = map[string]string{
   174  		fryKey:       composeAPIServerFry,
   175  		imageTagKey:  i.commonOptions.Tag,
   176  		namespaceKey: i.commonOptions.Namespace,
   177  	}
   178  	if i.networkOptions != nil &&
   179  		i.networkOptions.CustomTLSBundle != nil {
   180  		// hash tls data so that we can use that to dertermine if we need to re-deploy
   181  		dataToHash := append(append(i.networkOptions.CustomTLSBundle.ca, i.networkOptions.CustomTLSBundle.cert...), i.networkOptions.CustomTLSBundle.key...)
   182  		hash := sha1.Sum(dataToHash)
   183  		i.customTLSHash = hex.EncodeToString(hash[:])
   184  	}
   185  	coreClient, err := corev1.NewForConfig(config)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	i.coreClient = coreClient
   190  
   191  	rbacClient, err := rbacv1.NewForConfig(config)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	i.rbacClient = rbacClient
   196  
   197  	appsClient, err := appsv1.NewForConfig(config)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	i.appsClient = appsClient
   202  
   203  	aggregatorClient, err := kubeaggreagatorv1beta1.NewForConfig(config)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	i.aggregatorClient = aggregatorClient
   208  
   209  	client, err := discovery.NewDiscoveryClientForConfig(config)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	kubeVersion, err := client.ServerVersion()
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	return i, checkVersion(kubeVersion, minimumServerVersion)
   220  }
   221  
   222  // Status reports current installation status details
   223  type Status struct {
   224  	// True if there is a deployment with compose labels in the cluster
   225  	IsInstalled bool
   226  	// Tag of the installed components
   227  	Tag string
   228  	// Indicates if there is a legacy compose CRD in the system
   229  	IsCrdPresent bool
   230  	// Namespace in which components are deployed
   231  	Namespace string
   232  	// Image of the controller
   233  	ControllerImage string
   234  	// Image of the API service
   235  	APIServiceImage string
   236  	// Default service type for published services
   237  	DefaultServiceType string
   238  	// ControllerLabels contains all labels from Controller deployment
   239  	ControllerLabels map[string]string
   240  	// APIServiceLabels contains all labels from API service deployment
   241  	APIServiceLabels map[string]string
   242  	// APIServiceAnnotations contains annotations from the API service deployment
   243  	APIServiceAnnotations map[string]string
   244  }
   245  
   246  func (c *installer) isInstalled() (Status, error) {
   247  	crds, err := apiextensionsclient.NewForConfig(c.config)
   248  	if err != nil {
   249  		return Status{}, err
   250  	}
   251  	isCrdPresent := true
   252  	_, err = crds.CustomResourceDefinitions().Get("stacks.compose.docker.com", metav1.GetOptions{})
   253  	if err != nil {
   254  		if apierrors.IsNotFound(err) {
   255  			isCrdPresent = false
   256  		} else {
   257  			return Status{}, err
   258  		}
   259  	}
   260  	apps, err := c.appsClient.Deployments(metav1.NamespaceAll).List(metav1.ListOptions{
   261  		LabelSelector: everythingSelector,
   262  	})
   263  	if err != nil {
   264  		return Status{}, err
   265  	}
   266  	if len(apps.Items) == 0 {
   267  		return Status{
   268  			IsInstalled:  false,
   269  			IsCrdPresent: isCrdPresent,
   270  		}, nil
   271  	}
   272  	tag := ""
   273  	if apps.Items[0].Labels != nil {
   274  		tag = apps.Items[0].Labels[imageTagKey]
   275  	}
   276  
   277  	var apiServiceImage, controllerImage, defaultServiceType string
   278  	var controllerLabels, apiServiceLabels, apiServiceAnnotations map[string]string
   279  
   280  	for _, deploy := range apps.Items {
   281  		if deploy.Labels == nil {
   282  			continue
   283  		}
   284  		switch deploy.Labels[fryKey] {
   285  		case composeFry:
   286  			controllerImage = deploy.Spec.Template.Spec.Containers[0].Image
   287  			controllerLabels = deploy.Labels
   288  		case composeAPIServerFry:
   289  			apiServiceImage = deploy.Spec.Template.Spec.Containers[0].Image
   290  			apiServiceLabels = deploy.Labels
   291  			apiServiceAnnotations = deploy.Annotations
   292  		}
   293  		if svcType, ok := deploy.Labels[defaultServiceTypeKey]; ok {
   294  			defaultServiceType = svcType
   295  		}
   296  	}
   297  
   298  	return Status{
   299  		IsInstalled:           true,
   300  		Tag:                   tag,
   301  		IsCrdPresent:          isCrdPresent,
   302  		Namespace:             apps.Items[0].Namespace,
   303  		ControllerImage:       controllerImage,
   304  		APIServiceImage:       apiServiceImage,
   305  		DefaultServiceType:    defaultServiceType,
   306  		ControllerLabels:      controllerLabels,
   307  		APIServiceLabels:      apiServiceLabels,
   308  		APIServiceAnnotations: apiServiceAnnotations,
   309  	}, nil
   310  }
   311  
   312  func (s Status) match(c *installer) (bool, string) {
   313  	// make sure debug tags are never a match
   314  	if c.commonOptions.Tag == "debug" || s.Tag == "debug" {
   315  		return false, "force redeploy if desired or current state is in debug mode"
   316  	}
   317  	if s.Tag == c.commonOptions.Tag && s.DefaultServiceType == c.commonOptions.DefaultServiceType {
   318  		// check customTLSHash
   319  
   320  		if s.APIServiceAnnotations == nil && c.customTLSHash != "" {
   321  			return false, "Custom TLS hash mismatch"
   322  		}
   323  
   324  		if s.APIServiceAnnotations != nil {
   325  			actualValue := s.APIServiceAnnotations[customTLSHashAnnotationName]
   326  			if actualValue != c.customTLSHash {
   327  				return false, "Custom TLS hash mismatch"
   328  			}
   329  		}
   330  
   331  		// check custom matches
   332  		if c.customMatch == nil || c.customMatch(s) {
   333  			return true, fmt.Sprintf("Compose version %s is already installed in namespace %q with the same settings", c.commonOptions.Tag, s.Namespace)
   334  		}
   335  	}
   336  	return false, fmt.Sprintf("An older version is installed in namespace %q. Uninstalling...", s.Namespace)
   337  }
   338  
   339  func (c *installer) install(ctx context.Context) error {
   340  	log.Info("Checking installation state")
   341  	installStatus, err := c.isInstalled()
   342  	if err != nil {
   343  		return err
   344  	}
   345  	if err := c.validateOptions(); err != nil {
   346  		return err
   347  	}
   348  	if installStatus.IsInstalled && !c.controllerOnly {
   349  		match, message := installStatus.match(c)
   350  		log.Info(message)
   351  		if match {
   352  			return nil
   353  		}
   354  
   355  		if err = Uninstall(c.config, installStatus.Namespace, false); err != nil {
   356  			return err
   357  		}
   358  		if err = WaitForUninstallCompletion(ctx, c.config, installStatus.Namespace, false); err != nil {
   359  			return err
   360  		}
   361  	}
   362  	log.Infof("Install image with tag %q in namespace %q", c.commonOptions.Tag, c.commonOptions.Namespace)
   363  	ictx := &installerContext{}
   364  	var steps []func(*installerContext) error
   365  	if c.controllerOnly {
   366  		steps = []func(*installerContext) error{
   367  			c.createNamespace,
   368  			c.createPullSecretIfRequired,
   369  			c.createServiceAccount,
   370  			c.createClusterRoleBindings,
   371  			c.createController,
   372  		}
   373  	} else {
   374  		steps = []func(*installerContext) error{
   375  			c.createNamespace,
   376  			c.createPullSecretIfRequired,
   377  			c.createServiceAccount,
   378  			c.createClusterRoleBindings,
   379  			c.createEtcdSecret,
   380  			c.createNetworkSecret,
   381  			c.createAPIServer,
   382  		}
   383  		if !c.disableController {
   384  			steps = append(steps, c.createController)
   385  		}
   386  	}
   387  	steps = append(steps, c.createDefaultClusterRoles)
   388  	for _, step := range steps {
   389  		if err := step(ictx); err != nil {
   390  			return err
   391  		}
   392  	}
   393  	return nil
   394  }